Vela borrows Tailwind’s syntax, not its scope. What it ships is a Roblox-oriented mapping. Utilities
that correspond to real Roblox properties and helper instances, plus a few structural emulations
where the correspondence can be built honestly. Margins become a wrapper frame, and divide-*
becomes separator frames. Everything that cannot be expressed is rejected loudly. This page maps
where those edges are at 0.13.0.
What is in scope
Utilities apply to exactly eight host elements:
frame, scrollingframe, canvasgroup, textlabel, textbutton, textbox, imagelabel, imagebutton
They also apply to components — any tag starting with an uppercase letter, or any member
expression such as <Switch.Root />. A className there lowers into props and helper children on
the component itself, which reaches the screen only if the component forwards what it does not
consume. See How it works.
Anything that is neither is skipped, keeps its className verbatim, and reports
classname-on-unsupported-host. That covers lowercase intrinsics Vela does not implement, such as
screengui, and namespaced tags such as <svg:rect />, which never count as components.
Within those eight the covered ground is wide. Paint: colours (arbitrary hex and /N opacity
modifiers included), gradients, borders, rings and outlines, per-corner radius, shadows and opacity.
Space. Padding, margins, gap and space-*, divide-* separators, sizing with min and max
constraints, position on all four edges, anchor point, translation, rotation, scale, aspect ratio,
z-index and layout order. Layout: flex layout and flex items, grid with real cell sizing,
visibility, overflow and the ScrollingFrame families. Then typography, image scaling, interactivity
and overscroll, and arbitrary length values across most of it. Finally a TweenService-backed
motion layer with the runtime variants — hover:, active:, focus:, dark:, the breakpoints and
their max- complements, and the attribute-backed states you define yourself. opacity-* is the
one utility that reaches past the element it is written on
(how far, and where it stops). A config can add
utilities through plugins, and the
utilities reference enumerates all of it.
Two things cut across all of it. Every pixel offset a utility lowers is a rem unit following the viewport. And the whole set emits for Vide as well as React, with the same utilities, the same diagnostics, and a byte-identical static emit.
What is permanently out
Vela distinguishes a class it has never heard of, reported as unsupported-utility-family, from a
real Tailwind family Roblox UI cannot express, reported as no-roblox-equivalent. The second list
is a design decision, not a roadmap. Letter spacing, per-element filters, 3D transforms, the CSS
positioning model, grid spans, cursors, text indentation, and the rest of the
not-implemented table. Each entry names what
Roblox is missing.
The gaps that do not announce themselves
Most of Vela’s edges produce a diagnostic. The ones below do not, and they are worth reading twice.
Host-element restrictions are editor-only on the static path. Vela knows which utilities make sense on which tag. That table drives completions, hover and the unsupported-host-utility diagnostic in the language server. The static transform never consults it, so <frame className="text-red-500" /> compiles clean and emits TextColor3 on a Frame. Install the editor tooling to catch it. The runtime path does apply the rule, but by dropping the utility without saying so.
Components that drop what they are handed. If the component does not forward its unrecognized
props to a host element and render children, all of it vanishes. There is no warning in the editor
or the build.
A class value the compiler cannot read is unchecked. Where the final string is only known in
game, nothing warns about anything in it. bg-blu-600 is a diagnostic in a literal, and total
silence in `flex ${tone}`. Coverage is not the problem. The in-game resolver handles every family the static path does, and branches whose tokens are written out resolve at compile time with their diagnostics intact. What is left is genuinely opaque text. The theme is re-parsed from serialized text there too, so a value not shaped like Color3.fromRGB(r, g, b) or new UDim(a, b) degrades silently.
Dynamic class names covers the divergence.
Gradient stops set to transparent. They produce no diagnostic — the stop is dropped, and a two-stop gradient renders as a flat fill. Every other family without a transparency channel does warn.
One behavior worth knowing before you write anything
Roblox paints every GuiObject as an opaque grey box with a 1px border, and a framework that only adds properties can never take that back. So a supported host element carrying a className starts from BackgroundTransparency = 1 and BorderSizePixel = 0 instead.
Anything that paints opts back out: a bg-*, opacity-*, a gradient stop, or a BackgroundColor3
/ BackgroundTransparency prop of your own. A background painted by a variant reopens it at
runtime, so hover:bg-sky-500 still works on an otherwise transparent element. An element with no
className is untouched, and so is a component.
Anywhere the grey default was load-bearing, the element renders invisible. preflight: false restores the old behaviour wholesale.
Sharp edges still open
gap-* on a grid element emits a conflicting UIListLayout alongside the UIGridLayout it feeds
(utilities reference). And divide-* separators remain
sensitive to explicit LayoutOrder and still double-count the parent’s gap-*, a consequence of
separators being list items (utilities reference).
Three newer edges sit beside them. The static path emits <uishadow> in lowercase, which
Instance.new rejects. A plugin utility that reaches back into itself expands to nothing, without a
diagnostic. And an opacity-* arriving from an ancestor component composes over a
BackgroundTransparency you set as a prop. All five are tracked in the
release notes.
Release state
Every package in the repo is at 0.13.0, versioned in lockstep: vela-rbxts itself, the
@vela-rbxts/* scope, the three @rbxts/vela-runtime* packages, and the vela-rbxts-lsp VS Code
extension. A fix anywhere moves everything.
@rbxts/vela-runtime-core holds the target-neutral half: the resolution engine, theme
normalization, rem maths, rich text, and margin and divide computation. @rbxts/vela-runtime and
@rbxts/vela-runtime-vide are the React and Vide hosts over it. Rojo maps the whole
node_modules/@rbxts directory into a place, so keeping the hosts apart is what stops a Vide game
from shipping React.
Almost nothing is enforced at install time. The only peerDependencies are each host’s own UI
library, declared optional, and the only engines field is the VS Code extension’s
vscode: ^1.91.0. A mismatched roblox-ts or TypeScript fails at build rather than at install.
Prebuilt native binaries also miss two platforms: Windows on ARM and linux arm64 musl.
A performance note
Config discovery runs for every eligible source file — the walk up the tree, and a read of whatever config it lands on. That is what lets two directories carry different configs.
The expensive part is not per file: the loaded config is keyed on the file’s own text and resolved once per directory per build.
Keep vela.config.ts to a plain defineConfig call with literal values — no imports beyond
vela-rbxts, no computed theme generation, no side effects. It is executed as real code, and any
edit invalidates the cache. A JSON config skips the transpile step and is cached the same way.
See also
- How it works for the pipeline, the two lowering paths, and the rules that keep you on the good one.
- Release notes for what changed in each version.
- Diagnostics for every warning code and what triggers it.
- Troubleshooting for the symptom-first version of this page.
- Editor setup for the checks the compiler does not run.