Velareference

Release notes

What changed in each Vela release, and the sharp edges still open.

Every package moves in lockstep, so a fix anywhere bumps everything. Releases are tagged from v0.3.0, and the upstream CHANGELOG.md carries the per-package detail. Installing for the first time? You are on the current release — skip to what is 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.

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.

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.

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.

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.

/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. 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.

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.

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.

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 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.

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.
  • 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.

0.6.0

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

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 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<unknown>.

0.4.1

divide-* drew one separator too many whenever the element carried a utility that lowers a helper instance. Config errors name the key that failed, and tsx-parse-failed carries a real source range. Malformed configJson is an error rather than a silent fallback to the default theme.

0.4.0

Where hover:, arbitrary hex colors and /N opacity modifiers arrived. Breakpoints and orientation are broken in this release, as described above. If you are pinned to it, drive reflow from your own state. See Responsive and input variants.

Earlier

0.3.0 landed most of the surface this documentation describes at once. Far-edge positioning, content-*/self-*, order-*, grid, translate-*, space-x/y-*, ring/outline, leading-*, the case transforms and decorations, the whole motion layer, and the structural utilities.

0.2.1 added vela.config.json and the palette DEFAULT key, and moved the declaration file to src/vela-env.d.ts. src/vela-rbxts.d.ts collides with the package name under a baseUrl of src, so the augmentation silently never loaded.

0.2.0 was the first public release.

Still open

Known and unfixed as of 0.13.0.

gap-* on a grid element emits a conflicting UIListLayout alongside the UIGridLayout it correctly feeds. Set CellPadding as a prop and leave gap-* off (utilities reference).

divide-* separators are sensitive to explicit LayoutOrder and double-count the parent’s gap-* — a consequence of separators being list items (utilities reference).

The static path emits <uishadow> in lowercase, which Instance.new rejects, so a statically lowered shadow-* can fail to instantiate and unwind the tree around it. A dynamic class value on the same element is unaffected.

The host guards are not symmetric. The runtime path drops a utility the host element cannot carry. The static path warns unsupported-host-utility and writes it anyway. A build that ignores warnings reaches Roblox, which rejects the assignment far from the class list that caused it.

A fade that arrives as context composes over a transparency you declared yourself. A consumer reading the alpha at runtime cannot tell your prop from one Vela lowered. Overlapping siblings under one opacity-* also darken where they overlap.

A plugin utility that reaches back into itself expands to nothing — the depth cap terminates silently, with no diagnostic.

divide-transparent is refused while divide-white/10 resolves. The /N modifier moved to every family with a transparency channel in 0.12.0. The transparent keyword did not.

Under Vide, a m-* the compiler cannot see is warned rather than rendered, because a margin box is an instance above one Vide has already parented.

A theme value that is not a valid expression fails silently, three ways. Vela splices in the first expression that parses and drops the rest. A trailing comment is lost, "#3b82f6" becomes a type error, and "#a1b2c3" parses as a TypeScript private name and emits bare. See the theming guide.

TypeScript 7 is not supported. 7.x fails the host adapter’s call with compiler-invocation-failed: Cannot read properties of undefined (reading 'Latest') on the first file. Pin typescript@5.9.3 or 6.x. Nothing declares this as a peer range, so the install is clean and the build is not.

Platform coverage

Prebuilt binaries do not cover every target: there is no aarch64-pc-windows-msvc build, so Windows on ARM is unsupported, and no linux arm64 musl build either. On those two, build the compiler crate and the language server from source. Full target list in the API reference.

See also