` | Intersects your own props P with { className?: ClassValue }. Use it when a component of yours should forward className. |
### The global augmentation
Importing `vela-rbxts` has a side effect: it declares
```ts
declare global {
namespace React {
interface Attributes {
className?: ClassValue;
}
}
}
```
That augmentation is what makes `className` type-check on the host elements. It applies only once
the module is imported somewhere your program includes, typically a one-line `.d.ts`:
```ts title="src/vela-env.d.ts"
import "vela-rbxts";
```
Several helpers exist in `@vela-rbxts/config` but are **not** re-exported from the root. The
functions are `SHADES`, `PALETTE_DEFAULT_KEY`, `resolveThemeColors`, `mergeColorRegistry`,
`normalizeColorRegistry` and `normalizeColorScale`. The types are `Shade`, `PaletteKey`,
`PaletteDefaultKey`, `ThemeScale`, `ColorPalette`, `ColorValue`, `ColorScaleInput`, `ThemeColors`,
`ColorInputMap`, `ThemeConfig` and `ThemeConfigInput`. Reach into `@vela-rbxts/config` directly, and
expect them to be less stable.
## From `vela-rbxts/transformer`
A CommonJS module whose export is a callable program-transformer factory. It also carries
`.createTransformer`, `.createRbxtsTailwindProgramTransformer` and `.default`, all pointing at
itself, so every resolution style lands on the same function.
This is the value that goes in `tsconfig.json`:
```json title="tsconfig.json"
{
"compilerOptions": {
"plugins": [{ "transform": "vela-rbxts/transformer" }]
}
}
```
The factory throws if roblox-ts does not hand it a TypeScript instance. Plugin options are in
[Configuration](https://docs.astra-void.xyz/vela-rbxts/reference/config.md#tsconfig-plugin-options).
## Published packages
| Package | What it is | Install directly? |
|---|---|---|
| `vela-rbxts` | The entry point: config helpers, types, the global augmentation, the transformer subpath, and — since 0.11.0 — the `vela` CLI | **Yes** — this is the only one an app installs |
| `@vela-rbxts/compiler` | The Rust compiler behind an N-API binding: transform, completions, hover, diagnostics, document colors | No, pulled in transitively |
| `@vela-rbxts/compiler-wasm` | The same compiler built to WebAssembly, for hosts that cannot load the native addon — a browser, most usefully | Only if you are lowering classes outside Node |
| `@vela-rbxts/config` | The theme schema, `defineConfig`, `defaultConfig`, and the color merge helpers | Only to reach a helper the root package does not re-export |
| `@vela-rbxts/core` | Shared constants, including the supported host element tags | No — and nothing depends on it, so it never arrives transitively either |
| `@vela-rbxts/ir` | The intermediate style representation shared between the compiler and its consumers | No |
| `@vela-rbxts/types` | Shared TypeScript types | No |
| `@vela-rbxts/rbxtsc-host` | The roblox-ts adapter: file eligibility, config discovery, diagnostic mapping | No |
| `@rbxts/vela-runtime-core` | The target-neutral half of the runtime: resolution engine, theme normalization, rem math, rich text, margin and divide computation | No, pulled in by whichever host you emit for |
| `@rbxts/vela-runtime` | The React runtime host every transformed module imports, as one ModuleScript the place shares | No — it ships with `vela-rbxts` |
| `@rbxts/vela-runtime-vide` | The same host for Vide, imported instead when the project sets `framework: "vide"` | No — it ships with `vela-rbxts` |
| `@vela-rbxts/lsp` | The language server binary and its platform packages | Only if you wire up a non-VS Code editor yourself |
| `vela-rbxts-lsp` | The VS Code extension, published as a VSIX rather than to npm | Install from the marketplace as `astra-void.vela-rbxts-lsp` |
For almost every project: install `vela-rbxts`, add the transformer to `tsconfig.json`, install the
VS Code extension. Everything else arrives as a dependency.
The three `@rbxts/vela-runtime*` packages are the exception to "nothing of Vela's reaches your
place". They sit under the `@rbxts` scope because roblox-ts only resolves a package whose scope
directory is one of the project's `typeRoots`. That is the one every roblox-ts project lists and
every Rojo template maps. So they need no tsconfig or Rojo change. But pnpm needs
`nodeLinker: hoisted` for the directory to be real,
[covered in installation](https://docs.astra-void.xyz/vela-rbxts/getting-started/installation.md#install-the-packages).
Each host declares its own UI library as an **optional** peer, which keeps a Vide project from
installing React. The host you do not emit for is inert.
The root package also declares two `bin` entries, `vela` and `vela-rbxts`, pointing at the same CLI.
It lowers a source tree ahead of `rbxtsc` for projects that cannot register the transformer — see
[the CLI reference](https://docs.astra-void.xyz/vela-rbxts/reference/cli.md).
All packages are versioned in lockstep at **0.13.0**, the VS Code extension included. Never mix
versions across the `@vela-rbxts/*` scope. What changed in each release is in the
[release notes](https://docs.astra-void.xyz/vela-rbxts/reference/release-notes.md).
Three subpaths beyond the roots are public. `@vela-rbxts/rbxtsc-host/project-config` exposes config
discovery and loading, which the VS Code extension uses to evaluate the config files it watches.
`@vela-rbxts/compiler/native` exposes the raw N-API binding. Both roots also declare
`"./package.json"` in their exports maps. The root package ships a `schema.json` describing the
config input shape — point a `vela.config.json`'s `$schema` at it.
## Version requirements
**No package declares an `engines` field** except the VS Code extension, which requires VS Code
`^1.90.0`. **No package declares any `peerDependencies`** either. roblox-ts, TypeScript and React
are ordinary dependencies throughout, so nothing is checked at install time. You find out from a
build failure, and in the TypeScript case below, from one that does not mention versions.
What the project is developed and tested against:
| Tool | Version |
|---|---|
| Node | 24 (what CI runs) |
| TypeScript | 5.9.3, pinned exactly — see below |
| roblox-ts | `^3.0.0` |
| `@rbxts/react` | `^17.3.7-ts.1` |
| `@rbxts/services` | `^1.6.0` |
> **TypeScript is an upper bound, not just a floor**
>
> `@vela-rbxts/rbxtsc-host` resolves `typescript` from **your** project and calls into its API directly. TypeScript 7 moved enough of that API to break the call. A project on the current TypeScript fails on its first file with `compiler-invocation-failed: Cannot read properties of undefined (reading 'Latest')`.
>
> `5.9.3` is the tested pin and `6.x` also builds. roblox-ts's own `typescript` does not save you — it pins `=5.5.3` for itself, while Vela reads the one in your project.
`@rbxts/services` is a genuine runtime dependency: the runtime helper imports `UserInputService` and
`Workspace` from it to evaluate variants.
### Prebuilt compiler binaries
The compiler ships N-API binaries for these targets:
- `x86_64-unknown-linux-gnu`
- `x86_64-unknown-linux-musl`
- `aarch64-unknown-linux-gnu`
- `aarch64-apple-darwin`
- `x86_64-apple-darwin`
- `x86_64-pc-windows-msvc`
There is **no `aarch64-pc-windows-msvc` build** and no linux arm64 musl build, so those two targets
need the compiler crate built from source.
Two things look like escape hatches and are not. The napi-rs loader ends with a WASI fallback
reaching for `@vela-rbxts/compiler-wasm32-wasi`, which is not published. It always falls through to
`Cannot find native binding.`, whose advice to reinstall without a lockfile is misleading on a
platform that has no binary. And `@vela-rbxts/compiler-wasm` is a separate package with its own API,
not a drop-in fallback.
## See also
- [Configuration](https://docs.astra-void.xyz/vela-rbxts/reference/config.md) for the schema `defineConfig` accepts.
- [Diagnostics](https://docs.astra-void.xyz/vela-rbxts/reference/diagnostics.md) for what the transformer reports.
---
# Release notes
> What changed in each Vela release, and the sharp edges still open.
Source: https://docs.astra-void.xyz/vela-rbxts/reference/release-notes/
Every package moves in lockstep, so a fix anywhere bumps everything. Releases are tagged from
`v0.3.0`, and the upstream
[`CHANGELOG.md`](https://github.com/astra-void/vela-rbxts/blob/main/CHANGELOG.md) carries the
per-package detail. Installing for the first time? You are on the current release — skip to
[what is still open](#still-open).
## 0.13.0
The current release: variants you define yourself, responsive ranges, configurable breakpoints,
presets, per-corner radius, and transitions that reach the helper instances.
**State variants.** `addVariant("open", { attribute: "State", equals: "open" })` registers an
`open:` prefix that reads a Roblox attribute off the styled instance. `attr-[State=open]:` reads one
inline. Both compose with every other variant. Only the condition travels to the runtime, so
`open:rounded-lg` lowers `rounded-lg` exactly as a bare `rounded-lg` does. See
[Responsive and input variants](https://docs.astra-void.xyz/vela-rbxts/guides/responsive-and-input-variants.md).
**Responsive ranges.** `max-md:` is the exact complement of `md:`, minimum inclusive and maximum
exclusive, so the two cover every viewport once. They also chain: `md:max-lg:` addresses one bucket.
**Configurable breakpoints.** `theme.screens` is a theme axis like `colors`, so `tablet:` and
`max-tablet:` are two lines of config. The default scale gains `xl` (1280) and `2xl` (1536).
**Presets.** `presets: [gameUiPreset()]` folds a design system's theme, plugins, utilities and
variants into a project in one line. They resolve after the defaults and before the config naming
them, so a project always outranks what it pulled in. A JSON config can inline a preset but cannot
import one. See [presets](https://docs.astra-void.xyz/vela-rbxts/reference/config.md#presets).
**Per-corner radius.** `rounded-l-lg`, `rounded-t-md` and `rounded-tr-[0.625rem]` write the
individual `UICorner` properties. They **square off the corners they do not name**, so
`rounded-r-lg` alone keeps the left side flat. A directional utility beats the all-corner shorthand
on the corners it names, in either order.
**Transitions reach the helper instances.** `hover:rounded-xl` and `hover:border-blue-500` used to
snap, since a radius and a stroke live on `UICorner` and `UIStroke` rather than on the element. They
tween now, and `transition-shadow` joins the property groups. A custom motion driver receives those
tweens with a fourth argument naming the helper. It is additive, so a three-argument driver keeps
working.
**Optional inlay hints.** The editor can show what each class lowers to after the class. Off by
default, behind `velaRbxts.inlayHints.enabled`.
**A `vela.config.ts` is executed once per build**, resolved per directory and re-read only when it
changes. A config that throws is cached alongside one that resolves, so a typo is reported once and
the next edit lifts it.
**Three new prefix diagnostics.** `unknown-breakpoint` fires on a `max-` in front of something that
is not a breakpoint. `malformed-attribute-variant` fires on an `attr-[…]` that does not parse.
`invalid-breakpoint-range` fires on a chain whose bounds leave no viewport.
Sorting ranks the new variants in bands of their own. The existing variants keep the order they had.
## 0.12.8
**The published editor extension carried no config loader**, so a project's `vela.config.ts` was
never read and every key it defined was checked against the default theme. Upgrade if the editor
reports your own theme keys as unknown.
## 0.12.7
**`justify-stretch`**, which sets `UIListLayout.HorizontalFlex` to `Enum.UIFlexAlignment.Fill`.
`items-stretch` reached `VerticalFlex` from the start, but nothing reached the horizontal axis. A
column that wanted its children to fill the width had no class for it.
**A config the editor cannot read is now a notification** naming the file and the reason, rather than
a line in an output channel nobody opens.
## 0.12.6
Three fixes, all a walk that stopped at a boundary too early. The `SurfaceGui` pin and an
`opacity-*` fade now carry through a **fragment, a wrapper or a provider** rather than turning
around at one. A component exported without a name, as `export default (props) => …`, is read as a
component root like the named form beside it.
## 0.12.5
**A `SurfaceGui` keeps its literal pixels.** It takes its pixel space from the part it is drawn on
rather than from the viewport. A `BillboardGui` sizes itself the same way, so following the rem
curve there was wrong. Both are pinned now, on the static and runtime paths alike.
`theme.rem.pinnedUnder` names them, and emptying it puts them back on the curve. A container the
compiler never sees is outside what this reaches. Pin such a project with
`theme.rem: { min: 16, max: 16 }`.
## 0.12.4
**A margin side is one signed slot, so the last class written to it lands.** Padding and a negative
shift used to accumulate separately. Neither could overwrite the other. `-ml-2 -ml-2` shifted by 16
rather than 8, and `ml-4 -ml-2` applied both a padding *and* a shift. A side that ends up negative
moves the element. One that ends up positive pads it. `-mr-*` and `-mb-*` still report
`unsupported-negative-margin`.
**Sorting leaves the whitespace where its author put it**, so a class list written across several
lines is no longer flattened on every save.
Two smaller fixes. `placeholder-transparent` is no longer offered by completion, since Roblox has no
placeholder transparency to lower to. The language server exits on the `exit` notification instead
of waiting for the pipe to close.
## 0.12.3
A patch, mostly in the editor — but two of these move the compiler as well.
**A class an interpolation splices into is left alone.** `` `w-[${width}]` `` reaches the editor as
two pieces. Both were analyzed as whole classes, warning about a class you did not write.
**Whitespace inside an arbitrary value no longer splits it.** `w-[calc(100% - 4px)]` was read as
three classes and collected a diagnostic each. It reports the one it is owed now. The runtime splits
its class strings under the same rule, so a static class and a deferred one tokenize alike.
Four editor fixes behind them. A config pushed by the editor never arrived, so your theme keys
stayed unknown for the session. A file opening with a BOM answered one character to the left.
Completing inside a variant chain deleted the utility behind it. Sorting a value whose bracket never
closes rewrote it.
## 0.12.2
A patch, entirely in the editor — the compiler emits byte for byte what 0.12.0 did.
**The editor understands a class value written as a function.** `className={() => "bg-blue-600"}` is
how a Vide project writes a dynamic one. The walk had no arm for a function at all, so completions,
hover, diagnostics, swatches and sorting saw nothing in it. It follows what a function returns now,
along with template interpolations, `as const`, `satisfies`, string concatenation, and an object's
computed keys and spreads. Two interpolation bugs went with it. A half-typed file no longer reports
`${flag` as an unknown utility, and the sort keeps the whitespace either side of an interpolation.
## 0.12.1
The one to be on if you write Vide. **A Vide project that never named `framework` compiled against
the React host**, emitting an import of `@rbxts/vela-runtime` it has no reason to have installed.
The tsconfig inference only ran for a project with no `vela.config.ts` at all. A Vide project whose
config set a theme or a plugin silently lost it.
## 0.12.0
The largest release since 0.7.0. Offsets became rem units. The runtime became a package instead of a
copy. Branches started resolving at compile time, and Vela learned to emit for a second UI library.
> **Two of these change what you already have**
>
> **Rendering moves on any viewport but the base one**, because every pixel offset now scales. And
> **`className={a ? "x" : "y"}` now reports diagnostics it used to swallow**, so an upgrade can surface
> warnings on code that was already wrong. Neither needs a code change. Nothing about install changes.
**Every pixel offset a utility lowers is a rem unit.** `p-4`, `w-40`, `rounded-lg` and `text-sm` are
measured against the viewport. One rem is 16px at 1920×1020 and scales from there, with no provider,
hook or wrapper involved. Scale-valued utilities are untouched: `w-full` and `translate-x-1/2` stay
fractions of the parent. `TextSize` gains a ceiling of 100, where Roblox stops honouring it. To keep
literal pixels, close the clamp with `theme: { rem: { min: 16, max: 16 } }`. The compiler then drops
the scaling from the emit entirely. See [rem](https://docs.astra-void.xyz/vela-rbxts/guides/theming.md#rem).
**The runtime ships as `@rbxts/vela-runtime` instead of being copied into every file.** Ten
components used to carry ten copies of the same 5,500 lines. Each copy had its own camera
subscription. It is now one ModuleScript the whole place shares. In the reference app `App.luau`
went from 190,260 bytes to 27,912. Setup is unchanged — the package installs with `vela-rbxts` and
sits under the `@rbxts` scope every roblox-ts project already lists.
**The emit sends the theme you changed, not the whole palette.** A module whose host never has to
*parse* a class value sends its scales emptied entirely.
**A branch's classes are resolved at compile time.** `active ? "text-lg" : "text-sm"` names every
token it can apply. The compiler resolves all of them and hands the element the resolved props
alongside the tests that decide them. The full utility set applies inside a branch. A bad utility in
one reports a diagnostic instead of vanishing, and each test is evaluated once however many branches
hang on it. It reads ternaries, `&&`, the literal behind `||`, arrays and object maps. A branch
naming `m-*`, `divide-*`, `animate-*`, `transition*`, a text transform or `opacity-*` still takes
the whole value down the runtime path. See
[dynamic class names](https://docs.astra-void.xyz/vela-rbxts/guides/dynamic-class-names.md#a-branch-lowers-at-compile-time).
**`/N` opacity modifiers lower on every family with a transparency channel.** `border-slate-500/25`,
`divide-white/10` and the gradient stops previously lost exactly the alpha they were written for.
`placeholder-*` is the one family left, and its message now says why.
**Vela emits for [Vide](https://centau.github.io/vide/).** One config key, `framework: "vide"`, or
nothing at all — it is inferred from a `jsxFactory` beginning with `Vide.`. A statically lowered
element is identical under both targets. What differs is the runtime package the emit imports, and
that a dynamic class value is written as a thunk. Two limits are inherent. A component element's
prop names are fixed when it is called, and a `m-*` arriving out of an opaque call is warned rather
than rendered. See [the Vide guide](https://docs.astra-void.xyz/vela-rbxts/guides/vide.md).
Smaller things. Hovers and completions read offsets in rem, and arbitrary values read a `rem` unit.
A base helper a variant rule overwrote no longer leaves two `UIPadding` under one instance. The rule
prop parser learned `Vector2`, `ColorSequence`, `NumberSequence` and `Font`.
## 0.11.1
A patch. The runtime failed to typecheck in a project that sets `noUncheckedIndexedAccess`. That
matters because a `className` carrying a state variant pulls it into the emit, where **your**
compiler options check it.
## 0.11.0
The first release that does not change how a single class lowers. It makes the transformer optional.
**There is a `vela` command now**, for any project that cannot register a roblox-ts transform
plugin. A pinned toolchain, a build system that drives `tsc` itself, or a CI step that wants the
lowered sources as a reviewable artifact. `vela build` mirrors `src` into `.vela/src`, transforming
the files that use `className` and copying everything else byte for byte. `vela watch` re-transforms
on change. Both paths call the same compiler, so the output does not depend on which you pick. An
identical output is never rewritten, and pruning reads a manifest of what an earlier run emitted, so
a file the CLI never wrote is never deleted. See [the CLI reference](https://docs.astra-void.xyz/vela-rbxts/reference/cli.md).
**A whole-tree build stops re-evaluating `vela.config.ts` once per file.** Upgrading from 0.10.0
cannot change your output.
## 0.10.0
Finishes the `opacity-*` work 0.9.0 started, and stops the inlined runtime from crowding Luau's local
register limit.
**`opacity-*` crosses a component boundary, in both directions.** 0.9.0 faded a subtree by walking
the JSX, and that walk ends where the JSX does. A fade written *around* a component reached nothing
it rendered, and one written *on* a component stopped at `BackgroundTransparency`. The alpha now
travels as React context, through a provider that renders no instance, so the tree keeps its shape
and names. Providers are *relative*: two nested fades compose rather than the inner one winning.
**A class value that settles at render time is left whole to the runtime.** An `opacity-*` written
inside a recipe reaches the subtree it is written over.
**`opacity-unreachable-child` is gone.** One difference between the paths remains. The static path
leaves a transparency *you* declared as a prop alone, while a fade arriving as context composes over
both.
**A configured motion driver no longer takes the tree down at mount.** A driver written the
documented way compiles to a method carrying an implicit `self`. The runtime called it detached,
shifting every argument one place left. Methods are called as methods now, and the driver type
states them as methods. The arrow form is a compile error rather than a tree that disappears. See
[the motion driver](https://docs.astra-void.xyz/vela-rbxts/reference/config.md#motion-driver).
**The inlined runtime is grouped into namespaces**, taking the busiest register file in an emitted
file from 177 to 65 against Luau's cap of 200. Both the harness and the compiler crate assert a budget
of 120, so crowding the limit again fails a test rather than your build.
## 0.9.0
**`opacity-*` fades everything the element draws** — every channel the host paints, not just
`BackgroundTransparency`, which was invisible on a label whose background was already transparent.
**And into the subtree written under it.** Roblox has no inherited transparency, so the transformer
walks the JSX. Every instance below the class is handed the running product `1 - (1 - own) * alpha`.
Children written inside an expression count. A `canvasgroup` on the way down ends the walk. Where
this parts ways with a real composite is overlapping siblings: each is faded rather than the group,
so the overlap darkens.
**`opacity-*` stopped being order-dependent.** It is held until the whole class list is read, then
composed over whatever alpha the colours settled on. `opacity-50 bg-slate-700` and
`bg-slate-700 opacity-50` are the same thing.
**The inlined runtime stopped costing every file 96 registers.** It had been failing whole
components with `Out of local registers … exceeded limit 200`. That was against code nobody wrote.
If you are on 0.9.x and see `opacity-unreachable-child`, it is telling the truth — `{props.children}`
and component children stayed out of the compile-time walk. 0.10.0 carries the alpha across both.
## 0.8.0
**Every utility family resolves on the runtime class path.** The runtime host had implemented
roughly a third of what the static path lowers. A component whose `className` arrived as a *value*,
the normal shape for a variant recipe, silently lost most of the rest. Positioning, box constraints,
the grid, gradients, rings, shadows, `z-*`, transforms, `opacity-*`, the ScrollingFrame family and
most of the text families. All of them resolve dynamically now, with the static path's semantics.
This is the release where [dynamic class names](https://docs.astra-void.xyz/vela-rbxts/guides/dynamic-class-names.md) stopped
being a reduced dialect.
**A utility the host element cannot carry is dropped at runtime rather than applied**, since writing
`TextColor3` onto a `Frame` is a hard Roblox error. The static path still writes it — see
[still open](#still-open).
**The runtime host names `UIShadow` by its real class.** The lowercase spelling failed to instantiate
and unwound the whole tree. Fixed on the runtime path only.
## 0.7.0
The release that drew most of the current configuration surface.
- **Plugins.** `addUtilities` registers class names of your own, as a list of existing utilities or as
Roblox properties written directly, and `theme()` reads the resolved theme. Registered utilities take
variants, resolve on both paths, and sort *ahead* of the plain utilities. See
[`plugins`](https://docs.astra-void.xyz/vela-rbxts/reference/config.md#plugins).
- **A motion driver seam.** `setMotionDriver` names a module whose `transition`/`animate` methods
replace TweenService, one method at a time.
- **`dark:`, `active:` and `focus:` variants.** `dark:` reads a `VelaColorScheme` attribute off the
local player, since Roblox exposes no color scheme to a running game.
- **`theme.fontFamily` and `font-{family}`**, shipping `sans`, `serif` and `mono` and taking any
Roblox font family asset.
- **Scrolling frame utilities**, arbitrary length values (`[16px]`, `[50%]` and their negatives), and
real property filters on `transition-colors`/`-opacity`/`-transform`.
- **Class sorting**, exposed as the `source.sortVelaClasses` source action. See
[editor setup](https://docs.astra-void.xyz/vela-rbxts/guides/editor-setup.md#sorting-class-names).
## 0.6.0
> **Breaking-ish: grid cells are sized now**
>
> `UIGridLayout` stamps `CellSize` onto every child, and Vela's grid utilities never set it. Every cell fell back to Roblox's 100×100 default, so a `grid-cols-2` of 430px cards collapsed to 100px squares. `grid-cols-N` now divides the axis into N tracks and hands each cell its share of the gap.
>
> The cross axis needs its own answer, so **`auto-rows-*` and `auto-cols-*`** set it from the spacing
> scale. Without one it stays at 100px, so existing grids keep their row extent and gain correct track
> widths — anything compensating for 100px cells needs re-checking.
**`text-{color}` resolves on the runtime path.** The resolver had no `text-` branch at all. Every
text colour in a dynamic class value was dropped without a diagnostic. **`w-*` and `h-*` stop
erasing each other in variant rules**, carrying the 0.5.2 fix into `md:w-32 md:h-32`.
## 0.5.2
**`w-*` and `h-*` stop erasing each other on the runtime path.** `Size` holds both axes. A runtime
rule naming one used to state a whole `UDim2` and zero the other. Worth re-reading if you worked
around this: the axis that lost was left at zero, which also made a scale-based `rounded-full`
compute to nothing.
## 0.5.1
A version-only release — no compiler change.
## 0.5.0
> **Breaking: preflight neutralizes the Roblox host defaults**
>
> Roblox paints every `GuiObject` as an opaque gray box with a 1px border, and a framework that only
> ever *adds* properties can never take that back.
>
> A supported host element **that carries a `className`** now starts from `BackgroundTransparency = 1`
> and `BorderSizePixel = 0`. Anything that actually paints opts back out: a `bg-*` utility,
> `opacity-*`, a gradient stop, or a transparency you declared as a prop. A background painted by a
> variant reopens it at runtime. An element with no `className` is untouched, and so is a
> **component**.
>
> **Anywhere the gray default was load-bearing, the element now renders invisible.** Add the `bg-*` it
> relied on, or set [`preflight: false`](https://docs.astra-void.xyz/vela-rbxts/reference/config.md#preflight).
**`order-*` works under `flex`.** Vela's `UIListLayout` did not set `SortOrder`, and the engine
default is `Name`, so children sorted alphabetically and every `order-*` was a silent no-op. If you
still see `order-*` ignored, the layout instance is one you wrote yourself.
## 0.4.2
**A WebAssembly build of the compiler**, `@vela-rbxts/compiler-wasm`, so lowering can run in a
browser. The [playground](https://docs.astra-void.xyz/vela-playground/) compiles with it.
**Transitions tween from the right starting value**, instead of jumping to the target the first time
a variant introduced the prop. **A base opacity modifier no longer leaks into a variant**, and refs
on runtime-path elements are typed from the tag rather than as `Ref