Vela adds className to roblox-ts UI elements and compiles it away. You write Tailwind-shaped
utility classes on Roblox host elements and on your own components. A TypeScript transformer rewrites
them into Roblox properties and helper instances before roblox-ts emits Luau. p-4 becomes a
UIPadding child, rounded-lg a UICorner, bg-slate-700 a BackgroundColor3.
Vela emits for React by default, and for
Vide when your tsconfig.json compiles Vide JSX. Nothing survives into
the shipped game unless a class needs the player’s screen or input device to resolve.
import React from "@rbxts/react";
export function QuestCard() { return ( <frame className="flex flex-col gap-3 w-96 h-34 p-4 rounded-xl bg-slate-900 border border-slate-800"> <frame className="flex items-center gap-3 w-full h-9"> <frame className="w-9 h-9 rounded-lg bg-gradient-to-br from-amber-400 to-orange-600" /> <frame className="flex flex-col gap-1 w-64 h-9"> <textlabel className="w-full h-5 text-left text-slate-100 text-base font-semibold" Text="Quest complete" /> <textlabel className="w-full h-3 text-left text-slate-400 text-xs" Text="Deliver 12 sunpetal blooms" /> </frame> </frame>
<frame className="w-full h-2 rounded-full bg-slate-800"> <frame className="w-3/4 h-full rounded-full bg-gradient-to-r from-emerald-400 to-teal-500" /> </frame>
<frame className="flex justify-end items-center gap-2 w-full h-9"> <textbutton className="w-24 h-9 rounded-md bg-slate-800 border border-slate-700 text-slate-200 text-sm" Text="Dismiss" /> <textbutton className="w-28 h-9 rounded-md bg-emerald-500 text-white text-sm font-semibold" Text="Claim reward" /> </frame> </frame> );}import { __VelaBoundary } from "@rbxts/vela-runtime";import React from "@rbxts/react";export function QuestCard() { return <__VelaBoundary.Consume>{(<frame BackgroundColor3={Color3.fromRGB(15, 23, 43)} Size={UDim2.fromOffset(384, 136)} BorderSizePixel={0}><uilistlayout FillDirection={Enum.FillDirection.Vertical} Padding={new UDim(0, 12)} SortOrder={Enum.SortOrder.LayoutOrder}/><uipadding PaddingTop={new UDim(0, 16)} PaddingRight={new UDim(0, 16)} PaddingBottom={new UDim(0, 16)} PaddingLeft={new UDim(0, 16)}/><uicorner CornerRadius={new UDim(0, 12)}/><uistroke Thickness={1} Color={Color3.fromRGB(29, 41, 61)} Transparency={0}/> <frame Size={new UDim2(1, 0, 0, 36)} BorderSizePixel={0} BackgroundTransparency={1}><uilistlayout FillDirection={Enum.FillDirection.Horizontal} VerticalAlignment={Enum.VerticalAlignment.Center} Padding={new UDim(0, 12)} SortOrder={Enum.SortOrder.LayoutOrder}/> <frame Size={UDim2.fromOffset(36, 36)} BackgroundColor3={Color3.fromRGB(255, 255, 255)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0, 8)}/><uigradient Color={new ColorSequence(Color3.fromRGB(255, 185, 0), Color3.fromRGB(245, 73, 0))} Rotation={45}/></frame> <frame Size={UDim2.fromOffset(256, 36)} BorderSizePixel={0} BackgroundTransparency={1}><uilistlayout FillDirection={Enum.FillDirection.Vertical} Padding={new UDim(0, 4)} SortOrder={Enum.SortOrder.LayoutOrder}/> <textlabel Text="Quest complete" TextXAlignment={Enum.TextXAlignment.Left} TextColor3={Color3.fromRGB(241, 245, 249)} TextSize={16} Size={new UDim2(1, 0, 0, 20)} FontFace={new Font("rbxasset://fonts/families/SourceSansPro.json", Enum.FontWeight.SemiBold)} BorderSizePixel={0} BackgroundTransparency={1}/> <textlabel Text="Deliver 12 sunpetal blooms" TextXAlignment={Enum.TextXAlignment.Left} TextColor3={Color3.fromRGB(144, 161, 185)} TextSize={12} Size={new UDim2(1, 0, 0, 12)} BorderSizePixel={0} BackgroundTransparency={1}/> </frame> </frame>
<frame BackgroundColor3={Color3.fromRGB(29, 41, 61)} Size={new UDim2(1, 0, 0, 8)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0.5, 0)}/> <frame Size={UDim2.fromScale(0.75, 1)} BackgroundColor3={Color3.fromRGB(255, 255, 255)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0.5, 0)}/><uigradient Color={new ColorSequence(Color3.fromRGB(0, 212, 146), Color3.fromRGB(0, 187, 167))}/></frame> </frame>
<frame Size={new UDim2(1, 0, 0, 36)} BorderSizePixel={0} BackgroundTransparency={1}><uilistlayout FillDirection={Enum.FillDirection.Horizontal} HorizontalAlignment={Enum.HorizontalAlignment.Right} VerticalAlignment={Enum.VerticalAlignment.Center} Padding={new UDim(0, 8)} SortOrder={Enum.SortOrder.LayoutOrder}/> <textbutton Text="Dismiss" BackgroundColor3={Color3.fromRGB(29, 41, 61)} TextColor3={Color3.fromRGB(226, 232, 240)} TextSize={14} Size={UDim2.fromOffset(96, 36)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0, 6)}/><uistroke Thickness={1} Color={Color3.fromRGB(49, 65, 88)} Transparency={0}/></textbutton> <textbutton Text="Claim reward" BackgroundColor3={Color3.fromRGB(0, 188, 125)} TextColor3={Color3.fromRGB(255, 255, 255)} TextSize={14} Size={UDim2.fromOffset(112, 36)} FontFace={new Font("rbxasset://fonts/families/SourceSansPro.json", Enum.FontWeight.SemiBold)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0, 6)}/></textbutton> </frame> </frame>)}</__VelaBoundary.Consume>;}Every class on that card resolves at build time. Classes is the component as written. Lowered is the output: plain Roblox properties, a few helper instances, no trace of Vela. Every preview in these docs comes from the real compiler.
What you get
Static by default. A plain string of classes lowers entirely at compile time. The className
attribute disappears, the element keeps its tag, and Vela’s cost is paid once on your machine. Two
things opt an element into runtime work: a className the compiler cannot read, and
variants, which depend on the player’s device. A
class list that only branches still resolves at compile time, diagnostics intact.
Diagnostics instead of silence. A class Vela cannot lower is reported, not dropped.
tracking-wide warns that Roblox has no letter spacing. bg-brand says the palette needs a shade. A
typo in a theme key names the scale it searched. The
diagnostics reference lists every code. A few cases still fall
through silently, and the docs flag each one where it lives.
Editor tooling from the same compiler. The vela-rbxts-lsp language server links the same Rust
crate the build uses, so completions, hover, swatches, quickfixes and squiggles agree with the
compiler by construction. See editor setup.
A Roblox-shaped vocabulary. Vela is not Tailwind parity. It implements the slice that maps onto what Roblox gives you: colours, gradients, strokes, corners, padding, gap, sizing, position, flex layout and typography. Everything else is rejected loudly rather than approximated. The edges are mapped in scope and status.
Get started
Read these in order:
- Installation — the package, the tsconfig plugin, the config file, and a smoke test.
- Your first component — build a panel and read what each class lowered to.
- How it works — the pipeline and the two paths a
classNamecan take. - Scope and status — what is deliberately absent, and the gaps that do not announce themselves.
To skip setup, the playground runs the real compiler in the browser. Type class strings and watch them lower with no project at all.
Next
- Building something. Recipes covers buttons, badges, meters, list rows and a dialog, each with a live preview.
- Styling and layout. See theming, layout and sizing, colours and surfaces and text and fonts.
- Writing Vide. Vide covers the whole of the difference.
- A class did nothing. Troubleshooting is organised by symptom.
- Looking something up. The utilities reference lists every class and the values it accepts. Configuration, diagnostics and the API cover the rest.
<frame className="flex flex-col gap-4 p-4 w-80 rounded-lg bg-slate-800 border border-slate-700"> <textlabel className="text-lg font-semibold text-slate-50" Text="Party" /> <textbutton className="h-10 rounded-md bg-blue-600 text-white" Text="Invite" /></frame>Guides
Learn what lowers, and what doesn't.
Reference
Every class, value, and code.
Pre-1.0, and narrow on purpose.
Packages version in lockstep. Whole Tailwind families are not implemented, a dynamic className resolves against a much smaller utility set than a static one, and per-element restrictions are enforced by the editor rather than the compiler. Read the scope before you depend on a class.