Velaguides

Colors and surfaces

Colour utilities and the Roblox properties they write — fills, strokes, radius, shadows, gradients, opacity.

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.

src/client/Card.tsx
<frame className="bg-slate-800 rounded-lg border border-slate-700 shadow-lg">
<textlabel className="text-slate-100" Text="Ready" />
</frame>
A fill, a fill with a stroke, a pill radius, and a gradient — four utility families, four different targets.

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

FamilyRoblox propertyWritten on
bg-*BackgroundColor3the host instance
text-*TextColor3the host instance
image-*ImageColor3the host instance
placeholder-*PlaceholderColor3the host instance
border-{color}, ring-{color}, outline-{color}Colorthe shared UIStroke child
shadow-{color}Colora UIShadow child
divide-{color}BackgroundColor3the separator frames
from-*, via-*, to-*color keypointsa 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.

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:

CodeFires when
color-missing-shadeyou referenced a palette with no shade and that palette defines no DEFAULT
color-invalid-shadeyou gave a literal a shade (bg-white-500), or asked a palette for a shade it does not define
unknown-theme-keythe 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.

src/client/Panel.tsx
<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. UIStroke draws 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 like border-t-2 or border-x-slate-500. A UIStroke outlines the whole instance. There are no per-side strokes. (For dividers between children, divide-x/divide-y builds real separator frames — see the utilities reference.)
  • border-opacity-*. Use the /N modifier, as in border-slate-500/25, or border-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.

vela.config.ts
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.

ClassBlur radiusY offsetSpreadTransparency
shadow3100.9
shadow-sm2100.95
shadow-md64−10.9
shadow-lg1510−30.9
shadow-xl2520−50.9
shadow-2xl5025−120.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 suffixDirectionRotation
tto top270
trto top right315
rto right0
brto bottom right45
bto bottom90
blto bottom left135
lto left180
tlto top left225

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.
src/client/Header.tsx
<frame className="bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500" />
Two stops, three stops with via-*, and a vertical direction — each swatch is one UIGradient child.

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.

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.