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.
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.
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.
addUtilitiesregisters class names of your own, as a list of existing utilities or as Roblox properties written directly, andtheme()reads the resolved theme. Registered utilities take variants, resolve on both paths, and sort ahead of the plain utilities. Seeplugins. - A motion driver seam.
setMotionDrivernames a module whosetransition/animatemethods replace TweenService, one method at a time. dark:,active:andfocus:variants.dark:reads aVelaColorSchemeattribute off the local player, since Roblox exposes no color scheme to a running game.theme.fontFamilyandfont-{family}, shippingsans,serifandmonoand taking any Roblox font family asset.- Scrolling frame utilities, arbitrary length values (
[16px],[50%]and their negatives), and real property filters ontransition-colors/-opacity/-transform. - Class sorting, exposed as the
source.sortVelaClassessource action. See editor setup.
0.6.0
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
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.
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
The 0.4.0 runtime helper read Camera.ViewportSize once, at mount, and Roblox often still reports a 1×1 viewport then. sm:, md: and lg: never matched, and orientation was always landscape. 0.4.1 follows the camera’s signal, which fixes the initial evaluation and makes
breakpoints react to resizes. The input variants and hover: were never affected.
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
- Scope and status — what is in and out at 0.13.0.
- Configuration — the
preflightopt-out, andplugins. - Troubleshooting — symptom-first.