# Vide

> Emitting for Vide — no config to write, the same static output, and a thunk for anything dynamic.

Source: https://docs.astra-void.xyz/vela-rbxts/guides/vide/

Vela emits for [Vide](https://centau.github.io/vide/) as well as React, and everything this
documentation says about utilities, variants, the theme and the two lowering paths holds under
either target.

**A Vide project needs no Vela config for this.** With `framework` unset, Vela reads the target off
the `jsxFactory` a Vide project's `tsconfig.json` already sets:

```json title="tsconfig.json — enough on its own"
{
  "compilerOptions": {
    "jsx": "react",
    "jsxFactory": "Vide.jsx",
    "jsxFragmentFactory": "Vide.Fragment"
  }
}
```

Both runtime hosts ship with `vela-rbxts`, and the one you do not emit for is an inert ModuleScript.

The config key exists for saying it out loud rather than having it read — see
[naming the target explicitly](#naming-the-target-explicitly).

## The static path is the same output

A statically lowered element is *identical* source under both targets. Vide's JSX intrinsics are the
same lowercase Roblox class names React's are, and its props are Roblox instance properties
directly:

```tsx title="Both targets emit this"
<frame BackgroundColor3={Color3.fromRGB(49, 65, 88)} Size={__VelaRem.scale(UDim2.fromOffset(160, 0), 4)}>
  <uipadding PaddingTop={__VelaRem.scale(new UDim(0, 16), 0)} />
</frame>
```

The whole token-to-utility pipeline is shared: the same parser, theme, diagnostics, helper children
and [rem](https://docs.astra-void.xyz/vela-rbxts/guides/theming.md#rem) scaling. Two things differ. The module specifier at the
top of the file is `@rbxts/vela-runtime-vide` rather than `@rbxts/vela-runtime`, and the runtime
path behaves differently.

## A dynamic class value has to be a thunk

The one thing you write differently. A Vide component body runs **once**, so a class value that
changes has to be a function, like any other derived Vide prop:

```tsx title="src/client/Button.tsx"
const active = source(false);

// Tracked — the class list is re-read whenever `active` changes.
<textbutton className={() => (active() ? "bg-blue-600" : "bg-slate-700")} />

// Not tracked — read once, at the moment the component body ran.
<textbutton className={active() ? "bg-blue-600" : "bg-slate-700"} />
```

`className` is typed to allow both, as `ClassValue | (() => ClassValue)`, so the second line is not
an error. It is a class list that was correct once, and nothing warns about it.

The editor reads inside the thunk. Completions, hover, swatches, diagnostics and the class sort all
work on a deferred class value the way they do on a literal one.

> **A branch still resolves at compile time either way**
>
> A thunk is about *tracking*, not lowering. `() => (active() ? "bg-blue-600" : "bg-slate-700")` names both tokens in the source. Both are resolved by the compiler, and the element is handed the resolved props alongside the test, exactly as the React form is. Only the test itself is thunked.

Both targets resolve the branch into the same two `__velaRules`. What differs is the one line inside
`__velaTests`, and the runtime the host is imported from.

**Vide**

```tsx title="src/client/Button.tsx"
import Vide, { source } from "@rbxts/vide";

export function Button() {
  const active = source(false);

  return (
    <textbutton
      className={() => (active() ? "bg-blue-600" : "bg-slate-700")}
      Text="Ready"
    />
  );
}
```

```tsx title="Emitted for Vide"
import { createVelaRuntimeHost } from "@rbxts/vela-runtime-vide";
import type { VelaRuntimeHostComponent } from "@rbxts/vela-runtime-vide";
const VelaRuntimeHost = createVelaRuntimeHost({
    "preflight": true,
    "theme": {
        "colors": {},
        "radius": {},
        "spacing": {},
        "fontFamily": {},
        "screens": {},
        "rem": {
            "base": 16.0,
            "min": 8.0,
            "max": 64.0,
            "baseResolution": {
                "x": 1920.0,
                "y": 1020.0
            }
        },
        "replaced": [
            "colors",
            "radius",
            "spacing",
            "fontFamily",
            "screens"
        ]
    },
    "plugins": {
        "utilities": {}
    }
}) as unknown as VelaRuntimeHostComponent;
import Vide, { source } from "@rbxts/vide";
export function Button() {
    const active = source(false);
    return (<VelaRuntimeHost Text="Ready" BorderSizePixel={(0 as never)} BackgroundTransparency={(1 as never)} __velaRules={[
        {
            "condition": {
                "kind": "test",
                "index": 0,
                "expected": true
            },
            "effects": {
                "props": [
                    {
                        "name": "BackgroundColor3",
                        "value": "Color3.fromRGB(21, 93, 252)"
                    },
                    {
                        "name": "BackgroundTransparency",
                        "value": "0"
                    }
                ],
                "helpers": []
            }
        },
        {
            "condition": {
                "kind": "test",
                "index": 0,
                "expected": false
            },
            "effects": {
                "props": [
                    {
                        "name": "BackgroundColor3",
                        "value": "Color3.fromRGB(49, 65, 88)"
                    },
                    {
                        "name": "BackgroundTransparency",
                        "value": "0"
                    }
                ],
                "helpers": []
            }
        }
    ]} __velaTests={[
        ()=>active() ? true : false
    ]} __velaTag={"textbutton"}/>);
}
```

**React**

```tsx title="src/client/Button.tsx"
import React, { useState } from "@rbxts/react";

export function Button() {
  const [active] = useState(false);

  return (
    <textbutton
      className={active ? "bg-blue-600" : "bg-slate-700"}
      Text="Ready"
    />
  );
}
```

```tsx title="Emitted for React"
import { createVelaRuntimeHost } from "@rbxts/vela-runtime";
import type { VelaRuntimeHostComponent } from "@rbxts/vela-runtime";
const VelaRuntimeHost = createVelaRuntimeHost({
    "preflight": true,
    "theme": {
        "colors": {},
        "radius": {},
        "spacing": {},
        "fontFamily": {},
        "screens": {},
        "rem": {
            "base": 16.0,
            "min": 8.0,
            "max": 64.0,
            "baseResolution": {
                "x": 1920.0,
                "y": 1020.0
            }
        },
        "replaced": [
            "colors",
            "radius",
            "spacing",
            "fontFamily",
            "screens"
        ]
    },
    "plugins": {
        "utilities": {}
    }
}) as unknown as VelaRuntimeHostComponent;
import React, { useState } from "@rbxts/react";
export function Button() {
    const [active] = useState(false);
    return (<VelaRuntimeHost Text="Ready" BorderSizePixel={(0 as never)} BackgroundTransparency={(1 as never)} __velaRules={[
        {
            "condition": {
                "kind": "test",
                "index": 0,
                "expected": true
            },
            "effects": {
                "props": [
                    {
                        "name": "BackgroundColor3",
                        "value": "Color3.fromRGB(21, 93, 252)"
                    },
                    {
                        "name": "BackgroundTransparency",
                        "value": "0"
                    }
                ],
                "helpers": []
            }
        },
        {
            "condition": {
                "kind": "test",
                "index": 0,
                "expected": false
            },
            "effects": {
                "props": [
                    {
                        "name": "BackgroundColor3",
                        "value": "Color3.fromRGB(49, 65, 88)"
                    },
                    {
                        "name": "BackgroundTransparency",
                        "value": "0"
                    }
                ],
                "helpers": []
            }
        }
    ]} __velaTests={[
        active ? true : false
    ]} __velaTag={"textbutton"}/>);
}
```

## What the runtime host does differently

Nothing you have to think about, on a host element. The Vide host binds an effect that writes
whatever the current resolution names. A property that disappears goes back to what the element
declared, or to the class default. Interaction variants, `transition-*` and `animate-*`, the text
transforms, `divide-*` and the `opacity-*` model all behave as the React host's do, on the same
target-neutral core.

The reactive seams underneath are different, and two of their consequences reach you.

### A component element's props are fixed at the call

Vide hands a component its props once, rather than writing to an instance it owns. Which props the
host can pass down is decided when the component is called:

```tsx title="A prop that only a later reading names cannot appear"
<Card className={() => (loud() ? "bg-red-500 rounded-xl" : "bg-slate-700")} />
```

Both branches are read up front, so both `BackgroundColor3` and the `UICorner` are accounted for.
What cannot work is a name no reading at call time produced. A host element has no such limit — it
is an instance the host owns.

### A late `m-*` cannot be honoured

A margin is a wrapper instance *above* the element, and Vide parents an element as soon as it builds
one, so the box cannot be introduced afterwards. A class value naming `m-*` anywhere the compiler
can see it emits `__velaMarginBox`, and the host builds the box **before** the element.

**Vide**

```tsx title="src/client/Panel.tsx"
import Vide from "@rbxts/vide";

export function Panel(p: { big: () => boolean }) {
  return <frame className={() => (p.big() ? "m-4 p-2" : "p-2")} />;
}
```

```tsx title="Emitted for Vide"
import { createVelaRuntimeHost } from "@rbxts/vela-runtime-vide";
import type { VelaRuntimeHostComponent } from "@rbxts/vela-runtime-vide";
const VelaRuntimeHost = createVelaRuntimeHost({
    "preflight": true,
    "theme": {
        "colors": {},
        "radius": {},
        "spacing": {},
        "fontFamily": {},
        "screens": {},
        "rem": {
            "base": 16.0,
            "min": 8.0,
            "max": 64.0,
            "baseResolution": {
                "x": 1920.0,
                "y": 1020.0
            }
        }
    },
    "plugins": {
        "utilities": {}
    }
}) as unknown as VelaRuntimeHostComponent;
import Vide from "@rbxts/vide";
export function Panel(p: {
    big: () => boolean;
}) {
    return <VelaRuntimeHost className={()=>p.big() ? "m-4 p-2" : "p-2"} BorderSizePixel={(0 as never)} BackgroundTransparency={(1 as never)} __velaMarginBox={true} __velaTag={"frame"}/>;
}
```

**React**

```tsx title="src/client/Panel.tsx"
import React from "@rbxts/react";

export function Panel(props: { big: boolean }) {
  return <frame className={props.big ? "m-4 p-2" : "p-2"} />;
}
```

```tsx title="Emitted for React"
import { createVelaRuntimeHost } from "@rbxts/vela-runtime";
import type { VelaRuntimeHostComponent } from "@rbxts/vela-runtime";
const VelaRuntimeHost = createVelaRuntimeHost({
    "preflight": true,
    "theme": {
        "colors": {},
        "radius": {},
        "spacing": {},
        "fontFamily": {},
        "screens": {},
        "rem": {
            "base": 16.0,
            "min": 8.0,
            "max": 64.0,
            "baseResolution": {
                "x": 1920.0,
                "y": 1020.0
            }
        }
    },
    "plugins": {
        "utilities": {}
    }
}) as unknown as VelaRuntimeHostComponent;
import React from "@rbxts/react";
export function Panel(props: {
    big: boolean;
}) {
    return <VelaRuntimeHost className={props.big ? "m-4 p-2" : "p-2"} BorderSizePixel={(0 as never)} BackgroundTransparency={(1 as never)} __velaTag={"frame"}/>;
}
```

React emits nothing in that position, because the render that resolves a margin also renders the
wrapper around it.

What is left is a margin arriving out of an opaque call, where the runtime **warns** rather than
rendering the element unspaced.

## Naming the target explicitly

The inference is a walk, and it is worth knowing how far it reaches before overriding it. From each
source file's directory upward to the nearest `tsconfig.json`, following a **relative** `extends` up
to eight levels, reading `compilerOptions.jsxFactory`. A factory beginning with `Vide.` selects
Vide. Anything else, or none found, leaves the default of React.

Naming the key in `vela.config.ts` always wins, and it wins by being **present** rather than by its
value:

```ts title="vela.config.ts — only if you want to override the walk"
import { defineConfig } from "vela-rbxts";

export default defineConfig({ framework: "vide" });
```

So `framework: "react"` pins React even under a Vide `jsxFactory`. And `framework: "vide"` pins Vide
where the walk would not have found the factory: a tsconfig reached through a non-relative
`extends`, or one nested deeper than the walk goes. Both are escape hatches. A project whose
tsconfig says `Vide.` in the ordinary way has no reason to write either.

> **One target per project, not per file**
>
> `jsxFactory` is a program-wide TypeScript setting, so a project pointing it at Vide cannot compile React
> JSX at all. The choice is per project by construction. It is also why an unset `framework` is read as *the default* rather than as a positive request for React. A project that never mentioned Vela's config but compiles Vide JSX is asking for Vide.

## Writing the declaration file

Unchanged. The same side-effect import augments both namespaces, `React.Attributes` and
`Vide.Attributes`, so one file covers whichever you compile for:

```ts title="src/vela-env.d.ts"
import "vela-rbxts";
```

## See also

- [Dynamic class names](https://docs.astra-void.xyz/vela-rbxts/guides/dynamic-class-names.md) — what a branch costs and what it does
  not, on both targets.
- [`framework`](https://docs.astra-void.xyz/vela-rbxts/reference/config.md#framework) — the config key itself.
- [Release notes](https://docs.astra-void.xyz/vela-rbxts/reference/release-notes.md#0120) — what shipped in the Vide pass.
