Velareference

Utility reference

Every utility class, the exact values it accepts, and what it lowers to.

This is the complete list of what Vela compiles as of 0.13.0. If a class is not on this page, Vela does not implement it. An unknown family produces unsupported-utility-family, and a real Tailwind family with no Roblox equivalent produces no-roblox-equivalent. A few get misread as a colour key instead. Anything that looks like text-*. See Diagnostics for what each warning means.

Every utility lowers to a property, a helper instance Vela prepends as a child, or, for the structural utilities, extra frames from the runtime helper. The helpers are UIPadding, UIListLayout, UIGridLayout, UICorner, UIStroke, UIGradient, UISizeConstraint, UIAspectRatioConstraint, UIScale, UIShadow and UIFlexItem. One of each per element, shared by every utility targeting it: p-4 px-8 produces a single UIPadding, and border-*, ring-* and outline-* all share one UIStroke.

Most utilities resolve entirely at compile time. Three families are runtime-structural. Margins, divide-* and animate-* build wrappers, separators or loops that cannot be static props. Any of them moves the element onto the runtime path, even in a plain string literal. Each is marked below.

Supported elements

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

ElementNotes
frameLayout container
scrollingframeLayout container, plus overscroll-* and the scrolling families
canvasgroupLayout container; opacity-* fades the subtree as one composited layer and stops there
textlabelText properties allowed
textbuttonText properties allowed
textboxText properties allowed, plus placeholder-*
imagelabelimage-* and object-* allowed
imagebuttonimage-* and object-* allowed
Any componentAn uppercase tag (<Panel />) or a member expression (<Switch.Root />). Every utility is allowed

Anything else is skipped with its className left untouched, reporting 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. The warning names only the local part, so that one is reported as rect.

Text utilities are only meaningful on textlabel, textbutton and textbox. That covers text-* colour and size, font-*, italic, leading-*, alignment, wrapping, truncation, the case transforms and the decorations. image-* and object-* belong on imagelabel and imagebutton, placeholder-* on textbox, and overscroll-* and the scrolling families on scrollingframe.

The two paths enforce that differently. The runtime host drops a utility the host cannot carry. The static path warns unsupported-host-utility and writes it anyway, so <frame className="text-red-500" /> still emits a TextColor3 assignment onto a Frame — see still open.

On a component none of this applies — the host it renders is unknown, so every utility is offered. Whether the props land is up to the component. It must forward what it does not consume down to a host element, and nothing checks that it does. See How it works.

Color

ClassValuesRoblox targetNotes
bg-{color}theme color key, [#hex]BackgroundColor3 + BackgroundTransparency
text-{color}theme color key, [#hex]TextColor3 + TextTransparencyFallback branch of text-*
image-{color}theme color key, [#hex]ImageColor3 + ImageTransparency
placeholder-{color}theme color key, [#hex]PlaceholderColor3No transparency channel
border-{color}theme color key, [#hex]UIStroke.Color + UIStroke.TransparencyHelper instance
ring-{color}outline-{color}theme color key, [#hex]UIStroke.Color + UIStroke.TransparencySame UIStroke as border-*
shadow-{color}theme color key, [#hex]UIShadow.ColorHelper instance
divide-{color}theme color key, [#hex]separator BackgroundColor3 + BackgroundTransparencyruntime
from-{color}via-{color}to-{color}theme color key, [#hex]UIGradient.Color keypoints + UIGradient.TransparencyHelper instance

A colour key is either a literal, such as black or white, or a palette family plus one of eleven shades from 50 to 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 with shade color.

A palette may carry a DEFAULT, which a bare family name resolves to. Every built-in palette ships one mirroring its 500, so bg-slate and text-blue work with no config. A palette of your own resolves bare only if you give it a DEFAULT. Without one, the bare form is color-missing-shade. DEFAULT is not spellable in a class: bg-slate-DEFAULT is read as the single key slate-DEFAULT.

Arbitrary hex colours work in every colour family, as in bg-[#3b82f6] or text-[#fff]. Three- and six-digit payloads go straight to Color3.fromRGB(...). A bracket payload that is not a hex colour reports unsupported-arbitrary-value.

Opacity modifiers append /N to a color, where N is an integer from 0 to 100: bg-blue-600/50 emits the color plus BackgroundTransparency = 0.5. Every family Roblox gives a transparency channel lowers it:

Family/N lowers to
bg-*BackgroundTransparency
text-*TextTransparency
image-*ImageTransparency
shadow-*UIShadow.Transparency
border-*, ring-*, outline-*UIStroke.Transparency
divide-*the separator frames’ BackgroundTransparency
from-*, via-*, to-*a UIGradient.Transparency sequence whose keypoints line up with the color stops

A gradient’s keypoints line up per stop, so fading one does not fade its neighbours. placeholder-* is the one family left: Roblox has no placeholder transparency, so it reports unsupported-opacity-modifier.

Two keywords behave specially. transparent sets the transparency to 1 and removes the colour property, and on placeholder-* and divide-* it is unsupported-color-key. current and inherit are rejected outright. Note the asymmetry that leaves on divide-*: divide-white/10 resolves while divide-transparent is refused — the modifier and the keyword are separate parsers. Gradient stops report nothing at all: from-transparent, via-transparent and to-transparent are silently dropped, and the stop 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. A name that is not in the theme at all gives unknown-theme-key.

Border, ring, and outline

Everything here writes into a single shared UIStroke helper instance.

ClassValuesRoblox targetNotes
borderUIStroke.Thickness = 1Bare form
border-{n}0, 1, 2, 4UIStroke.ThicknessNo other numbers; use border-[3px]
border-transparentUIStroke.Transparency = 1
border-{join}round, bevel, miterUIStroke.LineJoinMode
border-{color}theme color key, [#hex]UIStroke.Color, Transparency = 0
ringUIStroke.Thickness = 3Also sets ApplyStrokeMode = Border
ring-{n}0, 1, 2, 4, 8UIStroke.Thickness
outlineUIStroke.Thickness = 2Also sets ApplyStrokeMode = Border
outline-noneoutline-hiddenUIStroke.Thickness = 0
border-[{n}]ring-[{n}]outline-[{n}]pixels, with or without the unitUIStroke.ThicknessArbitrary value, since 0.7.0. A bracket is read as a color first

border-*, ring-* and outline-* all merge into the same helper. On a collision the later token wins. They do not stack the way CSS rings stack on borders. The one real difference is that ring and outline set ApplyStrokeMode = Border, pinning the stroke to the element’s bounds. A bracketed thickness resolves on all three, with or without the unit: border-[3px], ring-[3px], outline-[2px]. A bracket is parsed as an arbitrary color first, so border-[#0f0] is the color and border-[3px] the thickness. See arbitrary values.

For border-*, four things are explicitly rejected with unsupported-border-value. The CSS border styles dashed, solid, dotted and double. Every side-specific form, such as border-x or border-t-2. border-opacity-*. And anything containing a slash. For ring-*/outline-*: inset, solid, dashed, dotted, double, offset-*, and any other unbracketed thickness (unsupported-stroke-value).

Radius

ClassValuesRoblox targetNotes
roundedUICorner.CornerRadiusThe theme's DEFAULT radius, 4px by default
rounded-{key}any key in theme.radiusUICorner.CornerRadiusPure theme lookup
rounded-[{n}]pixels, percentUICorner.CornerRadiusArbitrary value, since 0.7.0
rounded-t-{key}rounded-b-{key}any key in theme.radius, or a bracketed valueUICorner.TopLeftRadius + TopRightRadius, BottomLeftRadius + BottomRightRadiusSince 0.13.0. The other pair is squared to 0
rounded-l-{key}rounded-r-{key}any key in theme.radius, or a bracketed valueUICorner.TopLeftRadius + BottomLeftRadius, TopRightRadius + BottomRightRadiusSince 0.13.0. The other pair is squared to 0
rounded-tl-{key}rounded-tr-{key}rounded-bl-{key}rounded-br-{key}any key in theme.radius, or a bracketed valueThe one matching UICorner radius propertySince 0.13.0. Beats rounded-{key} on the corner it names, in either order

The default theme ships the DEFAULT plus ten named keys: none, xs, sm, md, lg, xl, 2xl, 3xl, 4xl, full. There is no numeric fallback, so rounded-8 is unknown-theme-key. A bracketed value is read directly: rounded-[6px] and rounded-[6] both give new UDim(0, 6). A directional form writes the individual corner properties, and squares off the corners it does not name — so rounded-r-lg alone keeps the left side flat. That makes it beat the all-corner shorthand on the corners it names, in either order:

Both round the left pair to lg and the right pair to md
<frame className="rounded-l-lg rounded-md" />
<frame className="rounded-md rounded-l-lg" />

A variant repaints what the base left open: rounded-l-lg hover:rounded-md.

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 key, [#hex]UIShadow.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 key, [#hex]UIGradient.Color

Stops flush into a ColorSequence. Rotation is emitted only when non-zero. BackgroundColor3 is forced to white so the gradient is not tinted by the element’s own background. That overrides a bg-* on the same element, regardless of token order. A bad direction is unsupported-gradient-direction.

Z-index

ClassValuesRoblox targetNotes
z-{n}0, 10, 20, 30, 40, 50ZIndexExactly these six
z-[{n}]any non-negative integerZIndexArbitrary value, since 0.7.0

z-[15] and z-[999] are how you reach a level the six presets do not cover. The payload has to be a whole number, because ZIndex is an integer: z-[1.5] keeps unsupported-arbitrary-z-index.

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

Padding

All padding writes into a single UIPadding helper instance.

ClassValuesRoblox target
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 resolves against theme.spacing first, then falls back to arithmetic: unsigned, finite, at least zero, a multiple of 0.5, giving new UDim(0, key * 4). So p-1.5 is 6 pixels, p-40 is 160, and p-0.25 is unknown-theme-key. The default theme contains exactly one spacing key, "4" — every other number comes from the fallback.

Arbitrary values

A bracketed payload names a value directly instead of going through a scale. Two kinds exist, and which one a family reads depends on the family.

Colors[#rgb] and [#rrggbb], in every color family. Lengths[16px], [16], [50%], [1rem] and their negatives. The unit is optional, and a bare number means pixels. A percentage becomes the scale half of a UDim, so w-[50%] is UDim2.fromScale(0.5, 0) and w-[120px] is UDim2.fromOffset(120, 0). A rem payload resolves against theme.rem.base and follows the viewport like any other offset, so text-[1.5rem] says what text-[24px] says.

FamilyReads a bracket asExample
Spacing — p-*, m-*, gap-*, space-*Lengthp-[12px], -mt-[8px]
Size — w-*, h-*, size-*, min-*, max-*Lengthw-[120px], h-[50%]
Position — left-*, top-*, inset-*, translate-*Lengthleft-[25%], -left-[10px]
Radius — rounded-*Lengthrounded-[6px]
Scrollbar width — scrollbar-w-*Lengthscrollbar-w-[6px]
text-*Length, read as TextSizetext-[13px]
leading-*Number, read as LineHeightleading-[1.6]
rotate-*Number, read as degreesrotate-[17deg]
z-*Integer, read as ZIndexz-[15]
border-*, ring-*, outline-*Length, read as Thicknessborder-[3px]
aspect-*Ratioaspect-[16/9]

Both paths resolve these identically. A payload the family cannot read reports unsupported-arbitrary-value and is dropped. z-[1.5] has its own unsupported-arbitrary-z-index, since ZIndex is an integer. A space inside the brackets does not end the class, so w-[calc(100% - 4px)] is one token reporting one diagnostic. (calc() is still not a payload any family reads.) The runtime splits its class strings under the same rule. A bracket that never closes still splits, which keeps the classes written after a typo applying.

Families not in the table above do not take a bracket. There is no arbitrary font-[…] weight, no arbitrary duration-[…], and no arbitrary grid template.

Margin

Roblox has no margin box, so Vela builds one. Positive margins wrap the element in a transparent frame padded by the margin values, with the element’s layout-facing props routed onto the wrapper. No static prop can express that, so any margin utility moves the element onto the runtime path, plain string literal or not.

ClassValuesEffectNotes
m-{key}spacing valuemargin box, all four sidesruntime
mx-{key}my-{key}spacing valuemargin box, one axisruntime
mt-{key}mr-{key}mb-{key}ml-{key}spacing valuemargin box, one sideruntime
-mt-{key}-ml-{key}spacing valuePosition shiftNegative top/left margins move, not wrap
-mr-{key}-mb-{key}unsupported-negative-margin
mx-autoAnchorPoint.X = 0.5, Position.X = 0.5 scaleStatic — centers without a wrapper
my-autoAnchorPoint.Y = 0.5, Position.Y = 0.5 scaleStatic

Margins sum with the parent’s gap-* — a gap-2 list whose children carry my-2 gives 16-pixel gaps, not 8. For space between list children, prefer gap-* on the parent: one property on one instance, and it keeps the element on the static path. A negative top or left margin moves the element rather than padding the box around it. Each side holds one signed value, and the last class written to it wins. ml-4 -ml-2 shifts by 8. -ml-2 ml-4 pads by 16. -ml-0 clears what came before it. -mr-* and -mb-* remain unsupported-negative-margin — those would have to pull the next sibling closer.

Gap and space

ClassValuesRoblox targetNotes
gap-{key}spacing valueUIListLayout.PaddingNo gap-x- or gap-y-
space-x-{key}spacing valueUIListLayout.Padding + FillDirection = Horizontal
space-y-{key}spacing valueUIListLayout.Padding + FillDirection = Vertical

Roblox’s UIListLayout has one padding axis, so there is no axis-specific gap to expose. space-x-* and space-y-* are the Tailwind spellings of the same property with a direction attached — space-y-2 is exactly flex flex-col gap-2. An unresolvable value is unsupported-space-value. space-x-reverse is not implemented.

On a grid element, gap-* also feeds UIGridLayout.CellPadding — see the grid caveat below.

Divide

divide-* inserts separator frames between the element’s content children — the Tailwind borders-between-children idiom. Like margins, separators are structural, so any divide utility moves the element onto the runtime path.

ClassValuesEffect
divide-xdivide-y1px separators between children, horizontal / vertical
divide-x-{n}divide-y-{n}0, 1, 2, 4, 8separator thickness
divide-{color}theme color key, [#hex]separator BackgroundColor3

Two sharp edges. Separators are themselves list items, so the parent’s gap-* applies on both sides of each separator — unlike CSS, where a divider lives inside the gap. And explicit LayoutOrder values on the children break separator ordering, because the separators are inserted by child position, not by layout order. divide-transparent reports unsupported-color-key (separators have no transparency channel), and an unresolvable payload is unsupported-divide-value.

Size

ClassValuesRoblox targetNotes
w-{value}px, full, fractions, spacing offsetsSize X componentMerges with h-
h-{value}px, full, fractions, spacing offsetsSize Y componentMerges with w-
size-{value}px, full, fractions, spacing offsetsboth axes
basis-{value}px, full, fractions, spacing offsetsSize X componentEffectively w-* regardless of fill direction
w-autow-fitAutomaticSize = Enum.AutomaticSize.X
h-autoh-fitAutomaticSize = Enum.AutomaticSize.Y
size-autosize-fitAutomaticSize = Enum.AutomaticSize.XYAlso when both axes are set

Accepted values on the sizing axes:

  • px — one pixel of offset.
  • full — scale 1.
  • Fractions come 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).

basis-* lowers onto the X axis of Size regardless of fill direction — it is w-* under another name. Use it only when porting Tailwind markup.

Constraints

ClassValuesRoblox target
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
right-{value}samePosition X, from the far edgeright-2 is new UDim(1, -8)
bottom-{value}samePosition Y, from the far edge
inset-{value}sameboth axes
-left-{value}-top-{value}-inset-{value}samenegated

The fraction set is the same allowlist as sizing. right-* and bottom-* position from the far edge but do not touch AnchorPoint, so the element’s top-left corner still lands there unless you anchor it. Pair them with origin-*: right-2 bottom-2 origin-bottom-right puts the bottom-right corner 8 pixels inside its parent’s.

A parent with a UIListLayout owns its children’s positions, so none of these classes take effect. Any flex, gap-* or space-* utility creates one. Remove the layout utilities from the parent 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.

ClassRoblox targetNotes
flexFillDirection = Enum.FillDirection.HorizontalBare form. Every form also sets SortOrder = LayoutOrder
flex-rowFillDirection = Enum.FillDirection.Horizontal
flex-colFillDirection = Enum.FillDirection.Vertical
flex-wrapWraps = true
flex-nowrapWraps = false
justify-startjustify-centerjustify-endHorizontalAlignment = Left, Center, Right
justify-betweenjustify-aroundjustify-evenlyHorizontalFlex = Enum.UIFlexAlignment.SpaceBetween, SpaceAround, SpaceEvenlyDifferent property
items-startitems-centeritems-endVerticalAlignment = Top, Center, Bottom
justify-stretchHorizontalFlex = Enum.UIFlexAlignment.FillDifferent property. Since 0.12.7
items-stretchVerticalFlex = Enum.UIFlexAlignment.FillDifferent property
content-{start,center,end}VerticalAlignment
content-{between,around,evenly,stretch}VerticalFlex

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

justify-* and items-* are named after the Roblox layout’s horizontal and vertical axes, not the flex main and cross axes, so they do not swap under flex-col. The fill alignments follow the same split: items-stretch writes VerticalFlex, justify-stretch writes HorizontalFlex.

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
self-{auto,start,center,end,stretch}ItemLineAlignment

There is no grow-{n}, no shrink-{n} — numeric variants are not recognized.

Grid

ClassValuesRoblox targetNotes
gridUIGridLayout child, SortOrder = LayoutOrder
grid-cols-{n}112FillDirection = Horizontal, FillDirectionMaxCells = n, CellSize.X
grid-rows-{n}112FillDirection = Vertical, FillDirectionMaxCells = n, CellSize.Y
auto-rows-{n}spacing scale, arbitraryCellSize.YCross axis of grid-cols-*
auto-cols-{n}spacing scale, arbitraryCellSize.XCross axis of grid-rows-*

Anything else in the grid family is not implemented, because UIGridLayout has no span concept. That includes col-span-*, row-span-* and template values. An unresolvable count is unsupported-grid-value.

grid-cols-* sizes the cells. UIGridLayout stamps CellSize onto every child and ignores the child’s own Size, so the count decides the track width. grid-cols-N divides the axis into N tracks and gives each cell its share of the gap:

grid grid-cols-2 gap-2.5
<uigridlayout
FillDirection={Enum.FillDirection.Horizontal}
FillDirectionMaxCells={2}
CellSize={new UDim2(0.5, -5, 0, 100)}
CellPadding={UDim2.fromOffset(10, 10)}
/>

The cross axis stays at the engine’s 100px until auto-rows-* names it. auto-cols-* does the same for grid-rows-*. Both read the spacing scale, so auto-rows-24 is 96px.

Order

ClassValuesRoblox target
order-{n}-order-{n}any integerLayoutOrder
order-firstLayoutOrder = -9999
order-lastLayoutOrder = 9999
order-noneLayoutOrder = 0

An unresolvable payload is unsupported-layout-order-value. Remember divide-* separators are positioned by child order, not LayoutOrder — mixing the two breaks separator placement.

LayoutOrder only decides anything when the parent’s layout sorts by it. Vela’s layouts set SortOrder = LayoutOrder, so order-* works under a flex or grid parent. A layout you wrote keeps the engine default of Name, which sorts alphabetically and ignores order-* — that was also Vela’s own bug until 0.5.0.

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
translate-x-{v}translate-y-{v}fractions or spacing valuesAnchorPoint or PositionA fraction anchors; a pixel value shifts

Degrees outside the list give unsupported-rotation-value. Scales outside the list give unsupported-scale-value. Scaling is uniform — there is no scale-x-*/scale-y-*.

translate-* splits by value kind. A fraction lowers to AnchorPoint, a shift measured in the element’s own size. A pixel value adds to the Position offset. So the CSS centering idiom works verbatim:

AnchorPoint (0.5, 0.5) + Position (0.5, 0.5) scale
<frame className="left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />

mx-auto/my-auto compile to the same centering — pick whichever reads better.

Effects

ClassValuesRoblox targetNotes
opacity-{n}any integer 0100Every transparency channel the element paints, and the subtree under itInverted: opacity-100 is fully opaque
opacity-{n}any integer 0100GroupTransparency, on a canvasgroupOne composited layer; the fade stops there

The value is converted to transparency as 100 - n. Any non-integer or out-of-range value gives unsupported-opacity-value.

It fades everything the element draws. That is BackgroundTransparency on every host, TextTransparency on the text hosts and ImageTransparency on the image hosts. It also covers the Transparency of a UIStroke or UIShadow the same class list drew.

And the subtree under it. Roblox has no inherited transparency. Vela hands every instance below the class a running product: 1 - (1 - own) * alpha. Children written inside an expression count, and so do {props.children} and component children, which the alpha reaches through React context. A canvasgroup ends the descent — its GroupTransparency composites the subtree in one pass.

It is order-independent, and multiplies with /N. The utility is held until the whole class list is read, then composed over whatever alpha the colours settled on. opacity-50 bg-blue-600/50 lands at 0.75 transparency rather than one winning. Use /N to fade one family, opacity-* for the element and its subtree.

It parts ways with a real CSS composite in two places. Overlapping siblings are faded individually, so the overlap darkens. And a fade arriving from an ancestor component composes over a BackgroundTransparency you declared as a prop. A canvasgroup gives the real composite in both cases.

Motion

Vela can describe transitions and looping animations, executed by TweenService inside the runtime helper.

ClassValuesEffectNotes
transitionTween property changes caused by variant rules
transition-allTween every property a rule changes
transition-colorsNarrow the tween to the color properties
transition-opacityNarrow the tween to the transparency properties
transition-transformNarrow the tween to Position, Rotation, UIScale
transition-shadowNarrow the tween to the UIShadow propertiesSince 0.13.0, when transitions began moving the helper instances
transition-noneDisable tweening
duration-{n}preset 751000, or any integer msTween time, n / 1000 seconds
delay-{n}preset 751000, or any integer msTween delay
ease-{linear,in,out,in-out}EasingStyle / EasingDirection
animate-{spin,pulse,bounce}Preset looping animationruntime
animate-noneNo animation

Defaults are 0.15 seconds and Quad/Out. duration-*, delay-* and ease-* each enable the transition on their own, so duration-200 alone is a complete transition spec.

Two constraints. A transition needs a property that changes, from a variant rule or a dynamic className. On a purely static element it is dropped with transition-without-runtime. And both families need a real instance to drive, so on a component they warn motion-on-component.

A hover transition, complete
<textbutton className="bg-sky-500 hover:bg-sky-600 transition duration-150" Text="Play" />

The property filters are real. Each holds the tween to its own group. A rule changing a colour and a position under transition-colors snaps the position and tweens the colour. Transitions reach the helper instances too, so hover:rounded-xl and hover:border-blue-500 tween rather than snapping, and transition-shadow is a working filter.

An unresolvable value is unsupported-transition-value or unsupported-animation-value.

A motion driver can replace TweenService for this whole section, one method at a time.

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, blackFontFace weight axisnormal is Regular, black is Heavy
font-{family}any key in theme.fontFamilysans, serif, mono by defaultFontFace family axisThe fallback branch of font-*
italicnot-italicFontFace style axisMerges with the weight
leading-{key}none 1, tight 1.25, snug 1.375, normal 1.5, relaxed 1.625, loose 2LineHeightNumeric forms are unsupported-line-height-value
text-lefttext-centertext-rightTextXAlignment
text-justifyunsupported-text-alignment
align-topalign-middlealign-bottomTextYAlignment = Top, Center, Bottom
text-wraptext-nowrapTextWrapped = true, false
whitespace-normalwhitespace-nowrapTextWrapped = true, falseAlias family; others are unsupported-whitespace-value
truncateTextTruncate = Enum.TextTruncate.AtEnd
uppercaselowercasecapitalizenormal-casethe Text string itselfRewritten at compile time when Text is a literal
underlineline-throughno-underlineRichText markup around TextBacks off with decoration-on-richtext if you set RichText

font-* is one prefix over three axes, and they merge into a single Font value. The emit is new Font(family, Enum.FontWeight.X, Enum.FontStyle.Italic), with the style argument present only when italic is. The prefix resolves the fixed weight names first and reads anything else as a key in theme.fontFamily, so font-bold font-mono italic lowers to one FontFace:

font-mono font-bold italic
FontFace={new Font("rbxasset://fonts/families/RobotoMono.json", Enum.FontWeight.Bold, Enum.FontStyle.Italic)}

The default scale is sans (Source Sans Pro), serif (Merriweather) and mono (Roboto Mono), and takes any Roblox font family asset, uploaded rbxassetid:// fonts included. Two weight names do not map one-to-one: normal becomes Regular, black becomes Heavy. Since the family lookup is the fallback, a font-* payload that is neither reports unknown-theme-key rather than a weight error.

Case transforms rewrite the string. When Text is a literal, the transform happens at compile time — <textlabel className="uppercase" Text="hello" /> emits Text="HELLO" and nothing else. When Text is an expression, the transform runs in the runtime helper instead.

Decorations use RichText. underline emits Text="<u>hello</u>" with RichText={true}, escaping as needed. If the element already sets RichText, Vela backs off and reports decoration-on-richtext.

Images

ClassRoblox target
object-coverScaleType = Enum.ScaleType.Crop
object-containScaleType = Enum.ScaleType.Fit
object-fillScaleType = Enum.ScaleType.Stretch
object-tileScaleType = Enum.ScaleType.Tile

object-tile is a Roblox-only extension with no Tailwind counterpart. Other values are unsupported-object-fit-value.

Interaction

ClassRoblox target
pointer-events-noneInteractable = false
pointer-events-autoInteractable = true
overscroll-autoElasticBehavior = Enum.ElasticBehavior.Always
overscroll-containElasticBehavior = Enum.ElasticBehavior.WhenScrollable
overscroll-noneElasticBehavior = Enum.ElasticBehavior.Never

overscroll-* is meaningful only on scrollingframe. Unresolvable values are unsupported-pointer-events-value and unsupported-overscroll-value.

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 and the scrolling families.

Scrolling

Four families, all meaningful only on scrollingframe.

ClassValuesRoblox targetNotes
scroll-{x,y,xy}ScrollingDirection = X, Y, XY
scroll-noneScrollingEnabled = falseDoes not set a direction
scrollbar-w-{n}spacing scale, arbitraryScrollBarThicknessscrollbar-w-2 is 8px
scrollbar-noneScrollBarThickness = 0
scrollbar-{color}any color, /N modifierScrollBarImageColor3, ScrollBarImageTransparency
canvas-{auto,auto-x,auto-y,none}AutomaticCanvasSize = XY, X, Y, None
A vertical list that grows its canvas
<scrollingframe className="scroll-y canvas-auto-y scrollbar-w-2 scrollbar-zinc-600/50" />
lowers to
<scrollingframe
ScrollingDirection={Enum.ScrollingDirection.Y}
ScrollBarThickness={8}
ScrollBarImageTransparency={0.5}
ScrollBarImageColor3={Color3.fromRGB(82, 82, 92)}
AutomaticCanvasSize={Enum.AutomaticSize.Y}
/>

canvas-* is what makes a scrollingframe measure its own content. Without it, the canvas stays at whatever CanvasSize the element declares and the scrollbar has nothing to travel over.

Tailwind’s own scroll-* utilities are a different family and are not implemented. scroll-smooth, scroll-m-* and friends report unsupported-scroll-value, naming the values that do work.

Variants

Variants 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 reports unknown-variant, names the supported set, and drops the token.

VariantConditionNotes
sm:md:lg:xl:2xl:Viewport width ≥ 640, 768, 1024, 1280, 1536xl and 2xl since 0.13.0. Every threshold is a theme.screens key
max-sm:max-md:max-lg:max-xl:max-2xl:Viewport width < the same thresholdSince 0.13.0. The exact complement of the bare form, so the two cover every viewport once. Chains, as md:max-lg:
attr-[{Name}={value}]:The styled instance carries that Roblox attributeSince 0.13.0. addVariant() registers the same condition under a name
portrait:Viewport width less than height
landscape:Viewport width ≥ height
touch:UserInputService.TouchEnabled, and no gamepad
mouse:Neither gamepad nor touch
gamepad:UserInputService.GamepadEnabled
hover:The pointer is over this element
active:This element is being pressed
focus:This element holds focus or selection
dark:Players.LocalPlayer carries VelaColorScheme = "dark"

Breakpoints are configurable. theme.screens is a theme axis like colors, so tablet: and max-tablet: are two lines of config. A max- form is the exact complement of the bare one, minimum inclusive and maximum exclusive, so the two cover every viewport once. They chain into a single bucket as md:max-lg:. A max- in front of something that is not a breakpoint is unknown-breakpoint. A chain whose bounds leave no viewport is invalid-breakpoint-range.

State variants are yours to define. attr-[State=open]: reads a Roblox attribute off the styled instance. addVariant("open", { attribute: "State", equals: "open" }) registers the same condition under a name. An attr-[…] that does not parse is malformed-attribute-variant. See Configuration.

A square viewport counts as landscape. Input mode resolves by priority: gamepad over touch, touch over mouse. Exactly one of touch, mouse and gamepad is ever active. hover and active are tracked per element, composed with any handlers you attached yourself. active clears on MouseLeave, since a release outside the element never reaches it. focus follows Focused/FocusLost on a textbox and SelectionGained/SelectionLost elsewhere. dark reads an attribute your app owns — Roblox exposes no color scheme to a running game.

There are still no structural variants (first:, last:) and no disabled:. 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 and for how to drive dark:.

Not implemented

Two diagnostics cover the ground Vela does not. A family it has never heard of is unsupported-utility-family — that bucket includes your typos. A real Tailwind family that cannot be expressed in Roblox UI is no-roblox-equivalent, and that list is permanent by design:

FamilyWhy there is no equivalent
tracking-*, indent-*, break-*, hyphens-*, list-*The Roblox text engine exposes no letter-spacing, indent, or line-breaking control
decoration-*, overlineNo RichText equivalent (underline and line-through do exist)
blur-*, backdrop-*, grayscale, invert, sepia, contrast-*No per-element filters — BlurEffect is camera-global
brightness-*, fill-*, stroke-*Deliberately excluded; an ImageColor3 approximation would mislead
skew-*, perspective-*Roblox UI is strictly 2D
static, fixed, absolute, relative, sticky, block, inlineRoblox positioning is always parent-relative; there is no positioning model to switch
float, clear, columns-*, col-span-*, row-span-*UIGridLayout has no span or flow concept
cursor-*, caret-*, accent-*, select-*, snap-*, resize-*No property to target
Tailwind’s scroll-* (scroll-smooth, scroll-m-*)The prefix is taken by the scrolling families; reports unsupported-scroll-value
ms-*, me-*No logical-direction margins
animate-pingNeeds a cloned element; the other animate-* presets exist

Families that exist in Vela but reject a given payload each have their own diagnostic — see the sections above and Diagnostics.

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. Every family on this page resolves there, with the static path’s semantics. Opacity modifiers, arbitrary values, and the families that only meet at the end. That last group is the two Size axes, Position and AnchorPoint, FontFace, and a grid track with the gap it gives back.

One thing differs, and it is the runtime path being stricter rather than thinner. A utility the host element cannot carry is dropped instead of written, since assigning TextColor3 to a Frame is a hard Roblox error. The static path writes it anyway. See supported elements.

opacity-* meets the two paths halfway. The compiler leaves the whole class list to the host, which resolves it and hands the children one alpha. An opacity-* written inside a variant recipe reaches the subtree it is written over.

Read Dynamic class names for the cost model and for what promotes an element onto this path.

See also

  • Configuration for the theme keys these utilities look up, and plugins for registering utilities of your own.
  • Diagnostics for every warning code named on this page.
  • Recipes for these utilities composed into real interface pieces.