Every colour utility resolves a key from theme.colors and writes it onto a specific Roblox
property. That is either the host instance or a helper Vela adds as a child. Nothing is inherited
and nothing cascades: a class writes one property on one instance.
<frame className="bg-slate-800 rounded-lg border border-slate-700 shadow-lg"> <textlabel className="text-slate-100" Text="Ready" /></frame>import React from "@rbxts/react";
export function Swatches() { return ( <frame className="flex gap-3 justify-center items-center w-96 h-24 p-4 rounded-lg bg-slate-900"> <frame className="w-16 h-16 rounded-md bg-slate-700" /> <frame className="w-16 h-16 rounded-md bg-slate-800 border-2 border-sky-500" /> <frame className="w-16 h-16 rounded-full bg-emerald-500" /> <frame className="w-16 h-16 rounded-md bg-gradient-to-b from-sky-500 to-indigo-600" /> </frame> );}import { __VelaBoundary } from "@rbxts/vela-runtime";import React from "@rbxts/react";export function Swatches() { return <__VelaBoundary.Consume>{(<frame BackgroundColor3={Color3.fromRGB(15, 23, 43)} Size={UDim2.fromOffset(384, 96)} BorderSizePixel={0}><uilistlayout FillDirection={Enum.FillDirection.Horizontal} Padding={new UDim(0, 12)} HorizontalAlignment={Enum.HorizontalAlignment.Center} VerticalAlignment={Enum.VerticalAlignment.Center} 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, 8)}/> <frame BackgroundColor3={Color3.fromRGB(49, 65, 88)} Size={UDim2.fromOffset(64, 64)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0, 6)}/></frame> <frame BackgroundColor3={Color3.fromRGB(29, 41, 61)} Size={UDim2.fromOffset(64, 64)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0, 6)}/><uistroke Thickness={2} Color={Color3.fromRGB(0, 166, 244)} Transparency={0}/></frame> <frame BackgroundColor3={Color3.fromRGB(0, 188, 125)} Size={UDim2.fromOffset(64, 64)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0.5, 0)}/></frame> <frame Size={UDim2.fromOffset(64, 64)} BackgroundColor3={Color3.fromRGB(255, 255, 255)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0, 6)}/><uigradient Color={new ColorSequence(Color3.fromRGB(0, 166, 244), Color3.fromRGB(79, 57, 246))} Rotation={90}/></frame> </frame>)}</__VelaBoundary.Consume>;}Open the Lowered tab on that preview to see the split the table below describes. bg-* lands on
the frame, while border-*, rounded-* and the gradient stops each become a child instance.
The color families
| Family | Roblox property | Written on |
|---|---|---|
bg-* | BackgroundColor3 | the host instance |
text-* | TextColor3 | the host instance |
image-* | ImageColor3 | the host instance |
placeholder-* | PlaceholderColor3 | the host instance |
border-{color}, ring-{color}, outline-{color} | Color | the shared UIStroke child |
shadow-{color} | Color | a UIShadow child |
divide-{color} | BackgroundColor3 | the separator frames |
from-*, via-*, to-* | color keypoints | a UIGradient child |
Each of these except placeholder-*, divide-* and the gradient stops has a paired transparency
property. Setting border-{color} writes Color and sets Transparency to 0, so a coloured
stroke is visible without a second class. shadow-{color} writes Color alone.
text-* is not only a color family. It also carries sizes, alignment, and wrapping — see Text and fonts for how the compiler decides which one you meant.
Arbitrary hex colors
Every colour family also accepts a bracketed hex payload, bypassing the theme: three- and six-digit
forms lower straight to Color3.fromRGB(...), and anything else reports
unsupported-arbitrary-value. Reach for these sparingly — a hex that appears twice is a theme key
waiting to be named in theming.
Opacity modifiers
A color can carry a level in one token: /N with an integer 0–100 sets the family’s transparency
alongside its color, Tailwind-style. bg-blue-600/50 emits BackgroundColor3 plus
BackgroundTransparency = 0.5. text-slate-100/80 fades text to 80% opacity.
It works on every family Roblox gives a transparency channel, which is all of them but one.
border-slate-500/25 sets the UIStroke’s Transparency, and divide-white/10 fades the
separator frames. from-blue-600/50 becomes a UIGradient.Transparency keypoint aligned with its
colour stop, so fading one stop leaves the others alone.
placeholder-* is the one family left. Roblox has no placeholder transparency, and fading the text
itself would take the typed value with it, so it still reports unsupported-opacity-modifier.
transparent, and the two rejected keywords
transparent is not a theme color. It is handled specially: it sets the family’s transparency property to 1 and removes the color property entirely. bg-transparent emits BackgroundTransparency = 1 and no BackgroundColor3.
Preflight already starts every classed host element at BackgroundTransparency = 1, so bg-transparent on a label or layout frame is a no-op. It still earns its place on an element whose background is painted by a variant and should be clear by default, and anywhere you set preflight: false. text-transparent, image-transparent and border-transparent are unaffected.
That is why placeholder-transparent is an error. PlaceholderColor3 has no paired transparency
property, so there is nothing for the utility to set. The compiler reports unsupported-color-key
rather than emitting something that would not work.
Gradient stops are the other family with no transparency property, and they produce no diagnostic at all. The stop is dropped and the gradient is built from what remains, so a two-stop gradient that quietly becomes one renders as a flat fill.
current and inherit are rejected outright with unsupported-color-key. Both are cascade
concepts, and there is no cascade on Roblox — a TextLabel does not learn its colour from an
ancestor Frame.
Shades and diagnostics
A colour payload is either a literal key or a palette key plus a shade. The suffix after the last
hyphen counts as a shade only if it is one of the eleven numbers. Anything else makes the whole
payload one semantic key, so bg-my-color looks up my-color.
A bare palette name resolves through the palette’s DEFAULT, and every built-in palette ships one
mirroring its 500. DEFAULT is a config key and never a class — bg-blue-DEFAULT is read as the
semantic key blue-DEFAULT and reported as unknown-theme-key.
Three diagnostics cover the failure modes:
| Code | Fires when |
|---|---|
color-missing-shade | you referenced a palette with no shade and that palette defines no DEFAULT |
color-invalid-shade | you gave a literal a shade (bg-white-500), or asked a palette for a shade it does not define |
unknown-theme-key | the name is not in theme.colors at all |
All three are warnings, not errors. The class is dropped and compilation continues. See Theming for what the default palette contains and how to add your own.
Borders
border-* builds a UIStroke child. Roblox strokes are a much narrower surface than CSS borders, and Vela’s border family reflects that narrowness rather than papering over it.
Thickness accepts exactly four values: border-0, border-1, border-2, and border-4. Bare
border is shorthand for thickness 1. Any other number, such as border-3 or border-8, is
rejected with unsupported-border-value.
border-transparent sets Transparency = 1. border-{color} sets Color and Transparency = 0.
border-round, border-bevel, and border-miter set LineJoinMode to Enum.LineJoinMode.Round, .Bevel, and .Miter. This family is easy to miss because it has no CSS analogue.
<frame className="border-2 border-slate-600 border-miter" />Everything below is explicitly detected and rejected with unsupported-border-value, so you get a diagnostic instead of a silent no-op:
- Border styles —
border-dashed,border-solid,border-dotted,border-double.UIStrokedraws one continuous line and has no style property. - Side-specific keys —
border-x,border-y,border-t,border-r,border-b,border-l, and their prefixed forms likeborder-t-2orborder-x-slate-500. AUIStrokeoutlines the whole instance. There are no per-side strokes. (For dividers between children,divide-x/divide-ybuilds real separator frames — see the utilities reference.) border-opacity-*. Use the/Nmodifier, as inborder-slate-500/25, orborder-transparent.
An arbitrary bracket thickness is not rejected: border-[3], border-[3px] and border-[0.125rem]
all reach UIStroke.Thickness, and like every other offset they scale with the viewport.
Rings and outlines
ring and outline exist for Tailwind muscle memory. ring is thickness 3, outline thickness 2,
and both set ApplyStrokeMode = Border. But Roblox gives one reliable UIStroke per instance, so
all three families write into the same helper and the later token wins. There is no CSS-style
ring stacked on a border. Off-list values are unsupported-stroke-value.
Radius
rounded-{key} sets CornerRadius on a UICorner child, and on the static path it is a pure theme lookup. The key must exist in theme.radius or you get unknown-theme-key on the radius family.
There are no arbitrary values on the static path: rounded-[8] reports unsupported-arbitrary-value and emits nothing. If you need a radius the theme does not have, add it to theme.extend.radius and give it a name.
theme: { extend: { radius: { card: "new UDim(0, 10)" } } }<frame className="rounded-card" />Shadows
shadow and the size presets build a UIShadow child. The presets are fixed. There is no way to
tune them from a class.
| Class | Blur radius | Y offset | Spread | Transparency |
|---|---|---|---|---|
shadow | 3 | 1 | 0 | 0.9 |
shadow-sm | 2 | 1 | 0 | 0.95 |
shadow-md | 6 | 4 | −1 | 0.9 |
shadow-lg | 15 | 10 | −3 | 0.9 |
shadow-xl | 25 | 20 | −5 | 0.9 |
shadow-2xl | 50 | 25 | −12 | 0.75 |
Blur becomes BlurRadius, offset becomes Offset, and spread becomes Spread — written only when
non-zero, which is why shadow and shadow-sm do not set it.
shadow-none sets Enabled = false on the UIShadow, disabling it rather than removing the instance.
shadow-inner is rejected with unsupported-shadow-inset. UIShadow draws outside the instance and has no inset mode.
shadow-{color} is a separate family that sets only the shadow’s Color, leaving whatever Transparency the preset established. The one exception is shadow-transparent, which writes Transparency = 1 and no color. It composes with a preset:
<frame className="shadow-lg shadow-slate-950" />Gradients
bg-gradient-to-{dir} creates a UIGradient child. bg-linear-to-{dir} is an accepted alias for the same thing.
Eight directions map to UIGradient rotations:
| Class suffix | Direction | Rotation |
|---|---|---|
t | to top | 270 |
tr | to top right | 315 |
r | to right | 0 |
br | to bottom right | 45 |
b | to bottom | 90 |
bl | to bottom left | 135 |
l | to left | 180 |
tl | to top left | 225 |
Rotation is only emitted when it is not 0, so bg-gradient-to-r leaves the property at its default. A direction outside the eight reports unsupported-gradient-direction.
Color stops come from from-*, via-*, and to-*, and how many you write changes the emitted ColorSequence:
- One stop becomes
new ColorSequence(c)— a flat sequence of a single color. - Two stops become
new ColorSequence(a, b). - Three stops become
new ColorSequence([keypoints])with explicit keypoints.
<frame className="bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500" />import React from "@rbxts/react";
export function Gradients() { return ( <frame className="flex gap-3 justify-center items-center w-96 h-24 p-4 rounded-lg bg-slate-900"> <frame className="w-24 h-16 rounded-md bg-gradient-to-r from-sky-500 to-indigo-600" /> <frame className="w-24 h-16 rounded-md bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500" /> <frame className="w-24 h-16 rounded-md bg-gradient-to-t from-emerald-500 to-teal-400" /> </frame> );}import { __VelaBoundary } from "@rbxts/vela-runtime";import React from "@rbxts/react";export function Gradients() { return <__VelaBoundary.Consume>{(<frame BackgroundColor3={Color3.fromRGB(15, 23, 43)} Size={UDim2.fromOffset(384, 96)} BorderSizePixel={0}><uilistlayout FillDirection={Enum.FillDirection.Horizontal} Padding={new UDim(0, 12)} HorizontalAlignment={Enum.HorizontalAlignment.Center} VerticalAlignment={Enum.VerticalAlignment.Center} 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, 8)}/> <frame Size={UDim2.fromOffset(96, 64)} BackgroundColor3={Color3.fromRGB(255, 255, 255)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0, 6)}/><uigradient Color={new ColorSequence(Color3.fromRGB(0, 166, 244), Color3.fromRGB(79, 57, 246))}/></frame> <frame Size={UDim2.fromOffset(96, 64)} BackgroundColor3={Color3.fromRGB(255, 255, 255)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0, 6)}/><uigradient Color={new ColorSequence([ new ColorSequenceKeypoint(0, Color3.fromRGB(97, 95, 255)), new ColorSequenceKeypoint(0.5, Color3.fromRGB(173, 70, 255)), new ColorSequenceKeypoint(1, Color3.fromRGB(246, 51, 154)) ])} Rotation={45}/></frame> <frame Size={UDim2.fromOffset(96, 64)} BackgroundColor3={Color3.fromRGB(255, 255, 255)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0, 6)}/><uigradient Color={new ColorSequence(Color3.fromRGB(0, 188, 125), Color3.fromRGB(0, 213, 190))} Rotation={270}/></frame> </frame>)}</__VelaBoundary.Consume>;}With a gradient present, Vela sets BackgroundColor3 to white, because UIGradient multiplies against the instance’s background and anything else would tint every stop. So a bg-* on the same element is overwritten — pick the gradient or the fill, not both.
Element opacity
opacity-N accepts any integer from 0 to 100, not a fixed step scale, and inverts it. Roblox
measures transparency where CSS measures opacity. opacity-100 is fully opaque, opacity-0 is
fully transparent, and opacity-40 emits 0.6.
A non-integer or out-of-range value reports unsupported-opacity-value. The editor’s completion list shows a shorter set of round numbers, but that list is suggestion sugar — opacity-37 compiles fine.
It fades everything the element draws, and everything under it. The value reaches every
transparency channel the host paints. It is then handed down the subtree as a running product,
1 - (1 - own) * alpha. That descent crosses component boundaries too, travelling as a React
context through {props.children}.
Reach for the /N modifier when you want a single family faded — text-slate-100/80 fades the
text and leaves the surface alone. The two multiply rather than fight: opacity-50 bg-blue-600/50
lands at 0.75 transparency, in either order.
opacity-* on a canvasgroup lowers to GroupTransparency, compositing the subtree in one pass and ending the descent there. Everywhere else Vela fades each instance individually, which is visibly different when siblings overlap — the overlap darkens. Wrap them in a canvasgroup when that shows.
See also
- Theming — the color, radius, and spacing scales these utilities read.
- Text and fonts — how
text-*disambiguates between color, size, alignment, and wrapping. - Utilities reference — the complete family list.
- Diagnostics — every code, and what triggers it.