Vela reference

Utility reference

Every utility class Vela supports, the exact values each one accepts, and the Roblox property or helper instance it lowers to.

This is the complete list of what Vela compiles. If a class is not on this page, Vela does not implement it — most unknown families produce an unsupported-utility-family warning, and a few (anything that looks like text-*) get misread as a color key instead. See Diagnostics for what each warning means.

Every utility lowers to one of two things: a property set directly on the host element, or a helper instance — a child instance Vela prepends to the element’s children so the property has somewhere to live. The helper instances are UIPadding, UIListLayout, UICorner, UIStroke, UIGradient, UISizeConstraint, UIAspectRatioConstraint, UIScale, UIShadow, and UIFlexItem. One helper of each kind is created per element and all utilities that target it write into the same instance, so p-4 px-8 produces a single UIPadding.

Supported elements

Vela lowers className on these eight intrinsic elements, and on components.

ElementNotes
frameLayout container
scrollingframeLayout container
canvasgroupLayout container
textlabelText properties allowed
textbuttonText properties allowed
textboxText properties allowed, plus placeholder-*
imagelabelimage-* allowed
imagebuttonimage-* allowed
Any componentAn uppercase tag (<Panel />) or a member expression (<Switch.Root />). Every utility is allowed

Any other JSX element — anything that is neither one of those eight tags nor a component — is skipped, its className is left in the output untouched, and the compiler reports classname-on-unsupported-host. That bucket is wider than it looks: it holds lowercase intrinsics Vela does not implement, such as screengui, and also namespaced tags such as <svg:rect />, which are never treated as components. The warning names only the local part of the tag, so <svg:rect /> is reported as rect.

Text utilities (text-* color and size, font-*, text-{left,center,right}, align-*, text-wrap, truncate) are only meaningful on textlabel, textbutton, and textbox; image-* only on imagelabel and imagebutton; placeholder-* only on textbox. Those restrictions are enforced only by the editor. The compiler does not check them, so <frame className="text-red-500" /> builds without complaint and emits a TextColor3 assignment on a Frame that has no such property.

On a component the restrictions do not apply at all: the host element it renders is unknown, so every utility is offered and unsupported-host-utility is never raised. Whether the props land is up to the component, which must forward the props and children it does not consume down to a host element — nothing checks that it does. See How it works.

Color

ClassValuesRoblox targetNotes
bg-{color}theme color keyBackgroundColor3 + BackgroundTransparency
text-{color}theme color keyTextColor3 + TextTransparencyFallback branch of text-*
image-{color}theme color keyImageColor3 + ImageTransparency
placeholder-{color}theme color keyPlaceholderColor3No transparency channel
border-{color}theme color keyUIStroke.Color + UIStroke.TransparencyHelper instance
shadow-{color}theme color keyUIShadow.ColorHelper instance
from-{color}, via-{color}, to-{color}theme color keyUIGradient.Color keypointsHelper instance, no transparency channel

A color key is either a literal (black, white, or anything you define as a plain string) or a palette family plus a shade from 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950. The shade is split off with the last - in the token, and only if the suffix is exactly one of those eleven numbers — so bg-my-color is looked up as the single key my-color, not as family my shade color.

A palette family may also carry a DEFAULT key, and a bare family name resolves to it — bg-slate is slate’s DEFAULT, the same convention Tailwind uses for nested color objects. Every built-in palette ships a DEFAULT mirroring its 500, so bg-slate, text-blue, border-rose, shadow-slate and from-sky all work with no config at all. A palette you define yourself resolves bare only if you give it a DEFAULT; without one, the bare form is color-missing-shade. Note that DEFAULT is not spellable in a class — only the eleven numeric suffixes count as shades, so bg-slate-DEFAULT is read as the single key slate-DEFAULT and gives unknown-theme-key.

Two keywords behave specially. transparent sets the transparency property to 1 and removes the color property entirely; on placeholder-*, which has no transparency property, it produces unsupported-color-key. current and inherit are rejected outright with unsupported-color-key.

Gradient stops are the other family with no transparency property, but they do not report anything: from-transparent, via-transparent and to-transparent are silently dropped, and the stop they would have contributed simply does not appear in the ColorSequence.

Getting the shade wrong is its own diagnostic: a palette with neither a shade nor a DEFAULT gives color-missing-shade, a literal with a shade or a palette missing that shade gives color-invalid-shade, and a name that is not in the theme at all gives unknown-theme-key.

Border

Everything here writes into a UIStroke helper instance.

ClassValuesRoblox targetNotes
borderUIStroke.Thickness = 1Bare form
border-{n}0, 1, 2, 4UIStroke.ThicknessNo other numbers
border-transparentUIStroke.Transparency = 1
border-{join}round, bevel, miterUIStroke.LineJoinMode
border-{color}theme color keyUIStroke.Color, Transparency = 0

Explicitly rejected with unsupported-border-value: the CSS border styles dashed, solid, dotted, double; every side-specific form (border-x, border-y, border-t, border-r, border-b, border-l and their -{n} variants); border-opacity-*; arbitrary values in brackets; anything containing a slash; and any numeric thickness other than 0, 1, 2, 4.

Radius

ClassValuesRoblox targetNotes
rounded-{key}any key in theme.radiusUICorner.CornerRadiusPure theme lookup

The default theme ships ten keys: none, xs, sm, md, lg, xl, 2xl, 3xl, 4xl, full. There is no numeric fallback and no arbitrary rounded-[8] on the compile-time path — an unknown key is unknown-theme-key in the radius family. The runtime path does accept brackets; see Dynamic class names below.

Shadow

ClassValuesRoblox targetNotes
shadowUIShadow presetblur 3, offset Y 1, spread 0, transparency 0.9
shadow-smUIShadow preset2 / 1 / 0 / 0.95
shadow-mdUIShadow preset6 / 4 / −1 / 0.9
shadow-lgUIShadow preset15 / 10 / −3 / 0.9
shadow-xlUIShadow preset25 / 20 / −5 / 0.9
shadow-2xlUIShadow preset50 / 25 / −12 / 0.75
shadow-noneUIShadow.Enabled = false
shadow-{color}theme color keyUIShadow.Colorshadow-transparent instead sets Transparency = 1
shadow-innerunsupported-shadow-inset

Presets emit BlurRadius = new UDim(0, blur), Offset = UDim2.fromOffset(0, offsetY), Spread only when it is non-zero, and Transparency.

Gradient

ClassValuesRoblox targetNotes
bg-gradient-to-{dir}t, tr, r, br, b, bl, l, tlUIGradient.RotationRotations 270, 315, 0, 45, 90, 135, 180, 225
bg-linear-to-{dir}samesameAccepted alias
from-{color}, via-{color}, to-{color}theme color keyUIGradient.Color

Stops flush into a ColorSequence: one stop becomes new ColorSequence(c), two become new ColorSequence(a, b), three become new ColorSequence([keypoints]). Rotation is only emitted when it is not 0, and BackgroundColor3 is forced to Color3.fromRGB(255, 255, 255) so the gradient is not tinted by the element’s own background. A bad direction is unsupported-gradient-direction.

Z-index

ClassValuesRoblox targetNotes
z-{n}0, 10, 20, 30, 40, 50ZIndexExactly these six

Everything else is a distinct diagnostic: z-auto gives unsupported-z-index-auto, z-[N] gives unsupported-arbitrary-z-index, a negative -z-N gives unsupported-negative-z-index, and any other number gives unsupported-z-index-value.

Padding

All padding writes into a single UIPadding helper instance.

ClassValuesRoblox targetNotes
p-{key}spacing valuePaddingTop, PaddingRight, PaddingBottom, PaddingLeft
px-{key}spacing valuePaddingLeft, PaddingRight
py-{key}spacing valuePaddingTop, PaddingBottom
pt-{key}spacing valuePaddingTop
pr-{key}spacing valuePaddingRight
pb-{key}spacing valuePaddingBottom
pl-{key}spacing valuePaddingLeft

Spacing values

A spacing key is resolved against theme.spacing first. If it is not there, Vela falls back to arithmetic: the key must not carry a sign, must parse as a finite number greater than or equal to zero, and must be a multiple of 0.5. The result is new UDim(0, key * 4).

So p-1.5 is 6 pixels, p-40 is 160 pixels, and p-0.25 is rejected with unknown-theme-key in the spacing family. The default theme contains exactly one spacing key, "4" — every other number you write is coming from this fallback, not from the theme.

Gap

ClassValuesRoblox targetNotes
gap-{key}spacing valueUIListLayout.PaddingNo gap-x- or gap-y-

Roblox’s UIListLayout has one padding axis, so there is no axis-specific gap to expose.

Size

ClassValuesRoblox targetNotes
w-{value}see belowSize X componentMerges with h-
h-{value}see belowSize Y componentMerges with w-
size-{value}see belowboth axes
w-auto, w-fitAutomaticSize = Enum.AutomaticSize.X
h-auto, h-fitAutomaticSize = Enum.AutomaticSize.Y
size-auto, size-fitAutomaticSize = Enum.AutomaticSize.XYAlso when both axes are set

Accepted values on the sizing axes:

  • px — one pixel of offset.
  • full — scale 1.
  • Fractions, from an exact allowlist: 1/2; 1/3, 2/3; 1/4, 3/4; 1/5, 2/5, 3/5, 4/5; 1/6, 5/6; and 1/12 through 11/12. Unreduced forms such as 2/4, 3/6, and 2/6 are not accepted.
  • Any spacing value, used as an offset. If the resolved spacing carries a non-zero scale component, it is rejected with unsupported-size-spacing-value.

w- and h- on the same element merge into one Size property. The emitted form depends on the values: both scales zero gives UDim2.fromOffset(x, y), both offsets zero gives UDim2.fromScale(x, y), and a mix gives the full UDim2.new(sx, ox, sy, oy).

Constraints

ClassValuesRoblox targetNotes
min-w-{key}spacing offsetUISizeConstraint.MinSize X
min-h-{key}spacing offsetUISizeConstraint.MinSize Y
max-w-{key}spacing offsetUISizeConstraint.MaxSize X
max-h-{key}spacing offsetUISizeConstraint.MaxSize Y

Emitted as new Vector2(x, y). An axis you do not specify gets a default: 0 for MinSize and math.huge for MaxSize. These are offset-only — no fractions, no full.

Position

ClassValuesRoblox targetNotes
left-{value}px, full, fractions, spacing offsetsPosition X
top-{value}samePosition Y
inset-{value}sameboth axes
-left-{value}, -top-{value}, -inset-{value}samenegated

The fraction set is the same allowlist as sizing. There is no right-* and no bottom-*; anchor from the other side with origin-* instead.

Anchor

ClassValuesRoblox targetNotes
origin-{key}top-left, top, top-right, left, center, right, bottom-left, bottom, bottom-rightAnchorPointEmitted as new Vector2(x, y)

Anything outside those nine keys is unsupported-anchor-value.

Flex layout

These write into a UIListLayout helper instance.

ClassValuesRoblox targetNotes
flexFillDirection = Enum.FillDirection.HorizontalBare form
flex-rowFillDirection = Enum.FillDirection.Horizontal
flex-colFillDirection = Enum.FillDirection.Vertical
flex-wrapWraps = true
flex-nowrapWraps = false
justify-start, justify-center, justify-endHorizontalAlignment = Left, Center, Right
justify-between, justify-around, justify-evenlyHorizontalFlex = Enum.UIFlexAlignment.SpaceBetween, SpaceAround, SpaceEvenlyDifferent property
items-start, items-center, items-endVerticalAlignment = Top, Center, Bottom
items-stretchVerticalFlex = Enum.UIFlexAlignment.FillDifferent property

Any other flex-{something} that is not a flex-item keyword is unsupported-flex-direction. An unresolvable justify-* or items-* value is unsupported-alignment-value.

Note that justify-* and items-* are named after the horizontal and vertical axes of the Roblox layout, not after the flex main and cross axes. They do not swap when you use flex-col.

Flex items

These write into a UIFlexItem helper instance on the child.

ClassRoblox target
flex-1FlexMode = Enum.UIFlexMode.Fill
flex-autoFlexMode = Enum.UIFlexMode.Fill
flex-initialFlexMode = Enum.UIFlexMode.Shrink
flex-noneFlexMode = Enum.UIFlexMode.None
growFlexMode = Enum.UIFlexMode.Grow
grow-0FlexMode = Enum.UIFlexMode.None
shrinkFlexMode = Enum.UIFlexMode.Shrink
shrink-0FlexMode = Enum.UIFlexMode.None

That is the exact and complete list. There is no grow-{n}, no shrink-{n}, and no basis-*.

Aspect ratio

ClassValuesRoblox targetNotes
aspect-squareUIAspectRatioConstraint.AspectRatio = 1
aspect-videoAspectRatio = 1.7777777778
aspect-[W/H]positive finite numbersAspectRatioArbitrary value
aspect-[N]positive finite numberAspectRatioArbitrary value
aspect-autounsupported-aspect-value

Arbitrary bracket values are accepted here on the compile-time path, unlike rounded-*.

Transform

ClassValuesRoblox targetNotes
rotate-{deg}, -rotate-{deg}0, 1, 2, 3, 6, 12, 45, 90, 180Rotation-rotate-0 stays 0
scale-{n}0, 50, 75, 90, 95, 100, 105, 110, 125, 150UIScale.ScaleMaps to 0, 0.5, 0.75, 0.9, 0.95, 1, 1.05, 1.1, 1.25, 1.5

Degrees outside the list give unsupported-rotation-value; scales outside the list give unsupported-scale-value.

Effects

ClassValuesRoblox targetNotes
opacity-{n}any integer 0100BackgroundTransparencyInverted: opacity-100 is fully opaque

The value is converted to transparency as 100 - n and formatted as 0 for fully opaque, 1 for fully transparent, one decimal place for multiples of ten, and two decimal places otherwise. Any non-integer or out-of-range value gives unsupported-opacity-value.

opacity-* only touches BackgroundTransparency. Text, image, and stroke transparency come from the corresponding text-transparent, image-transparent, and border-transparent forms, which set the channel to fully transparent rather than to a level.

Typography

ClassValuesRoblox targetNotes
text-{size}xs 12, sm 14, base 16, lg 18, xl 20, 2xl 24, 3xl 30, 4xl 36, 5xl 48, 6xl 60, 7xl 72, 8xl 96, 9xl 128TextSizePixel values shown
font-{weight}thin, extralight, light, normal, medium, semibold, bold, extrabold, blackFontFaceSee below
text-left, text-center, text-rightTextXAlignment
text-justifyunsupported-text-alignment
align-top, align-middle, align-bottomTextYAlignment = Top, Center, Bottom
text-wrap, text-nowrapTextWrapped = true, false
truncateTextTruncate = Enum.TextTruncate.AtEnd

font-* emits a complete Font value: new Font("rbxasset://fonts/families/SourceSansPro.json", Enum.FontWeight.X). The family is fixed — there is no font-sans/font-mono family selection and no way to point at a different font asset through a utility. Two weight names do not map one-to-one: normal becomes Regular and black becomes Heavy. An unknown weight gives unsupported-font-weight.

Visibility

ClassRoblox target
hiddenVisible = false
visibleVisible = true

Overflow

ClassRoblox target
overflow-hiddenClipsDescendants = true
overflow-clipClipsDescendants = true
overflow-visibleClipsDescendants = false

Any other overflow-* value gives unsupported-overflow-value. There is no overflow-auto and no overflow-scroll — use a scrollingframe for scrolling.

Variants

Eight variant prefixes are supported. They chain with colons, and multiple prefixes on one token combine with AND — md:touch:w-full applies only when both conditions hold. An unrecognized prefix makes the whole token fail to split, after which it parses as a plain utility and usually ends up as unsupported-utility-family.

VariantCondition
smViewport width ≥ 640
mdViewport width ≥ 768
lgViewport width ≥ 1024
portraitViewport width less than height
landscapeViewport width ≥ height
touchUserInputService.TouchEnabled, and no gamepad
mouseNeither gamepad nor touch
gamepadUserInputService.GamepadEnabled

All three breakpoints are minimum-width only; there is no maximum-width form and no xl/2xl. A square viewport counts as landscape. Input mode is resolved by priority — gamepad wins over touch, touch wins over mouse — so exactly one of touch, mouse, and gamepad is ever active.

Any variant-prefixed token forces the runtime helper into the module, even inside a plain string literal. See Responsive and input variants for the cost model.

Not implemented

These families are not recognized at all. They produce unsupported-utility-family and are dropped from the output.

FamilyWhy, and what to use instead
m-* and every margin variant (mx-, my-, mt-, mr-, mb-, ml-)Roblox has no margin box. Pad the parent, or use gap-* on a list layout.
absolute, relativeRoblox positioning is always absolute-within-parent. Use top-*/left-*/inset-* and origin-*.
right-*, bottom-*Only the left and top edges are exposed. Set origin-* to anchor from the other side.
grid-*No grid lowering exists. Use UIGridLayout directly.
ring-*Use border and border-{color}, which target UIStroke.
blur-*No blur lowering exists.
leading-*, tracking-*Roblox text has no line-height or letter-spacing property to target.
uppercase, lowercase, capitalizeNo text-transform property exists. Transform the string in code.
transition-*, duration-*, ease-*, animate-*Vela does not emit tweens. Animate with TweenService or a motion library.
transform, translate-*, skew-*Only rotate-* and scale-* are lowered. Translation is top-*/left-*.
content-*, self-*, place-*Not mapped onto the UIListLayout/UIFlexItem model.

Dynamic class names

When className is a dynamic expression rather than something Vela can collapse at compile time, the class string is resolved by the injected runtime helper instead. That helper implements a much smaller subset — fifteen prefixes and nothing else:

border (bare), border-*, bg-*, rounded-*, p-, px-, py-, pt-, pr-, pb-, pl-, gap-, w-, h-, size-.

Any other utility inside a dynamic className is silently dropped. There is no diagnostic, because the compiler never sees the final string. Three further differences from the compile-time path:

  • The runtime path accepts arbitrary bracket values that the compile-time path rejects: rounded-[8], p-[10], w-[240].
  • The auto and fit sizing keywords return nothing on the runtime path, so w-auto, h-auto, size-auto, w-fit, h-fit and size-fit never set AutomaticSize there.
  • w- and h- each emit a whole Size property, so the later one overwrites the earlier instead of merging into one value.

Read Dynamic class names before you build class strings at runtime — the gap between the two paths is the single most common source of “my class did nothing” reports.

See also