The other guides explain Vela one utility family at a time. This page builds the small pieces most
Roblox interfaces are made of. Every preview is compiled by the same compiler rbxtsc loads:
Classes for the source, Lowered for what comes out. Every example stays on the
static path.
Two conventions repeat throughout, both worth internalizing before you copy anything:
- Everything has an explicit size. Roblox instances default to zero-height
Sizevalues, which is why the examples writew-*andh-*on almost everything. The noise is Roblox’s defaults, not Vela’s requirements. Backgrounds are not in that bucket any more: since 0.5.0 preflight starts every classed element transparent, so a label that should not paint needs nobg-transparent. - Rows and columns are
UIListLayout. Any element withflex,gap-*,justify-*oritems-*on it gets exactly oneUIListLayoutchild. That layout owns the positions of every child under it. See layout and sizing for the full model.
Buttons
import React from "@rbxts/react";
export function Buttons() { return ( <frame className="flex gap-3 justify-center items-center w-96 h-20 p-4 rounded-lg bg-slate-900"> <textbutton className="w-28 h-10 rounded-md bg-sky-500 text-white text-sm font-semibold" Text="Play" /> <textbutton className="w-28 h-10 rounded-md bg-slate-800 border border-slate-600 text-slate-100 text-sm" Text="Options" /> <textbutton className="w-28 h-10 rounded-md bg-rose-600 text-white text-sm font-semibold" Text="Leave" /> </frame> );}import { __VelaBoundary } from "@rbxts/vela-runtime";import React from "@rbxts/react";export function Buttons() { return <__VelaBoundary.Consume>{(<frame BackgroundColor3={Color3.fromRGB(15, 23, 43)} Size={UDim2.fromOffset(384, 80)} 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)}/> <textbutton Text="Play" BackgroundColor3={Color3.fromRGB(0, 166, 244)} TextColor3={Color3.fromRGB(255, 255, 255)} TextSize={14} Size={UDim2.fromOffset(112, 40)} FontFace={new Font("rbxasset://fonts/families/SourceSansPro.json", Enum.FontWeight.SemiBold)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0, 6)}/></textbutton> <textbutton Text="Options" BackgroundColor3={Color3.fromRGB(29, 41, 61)} TextColor3={Color3.fromRGB(241, 245, 249)} TextSize={14} Size={UDim2.fromOffset(112, 40)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0, 6)}/><uistroke Thickness={1} Color={Color3.fromRGB(69, 85, 108)} Transparency={0}/></textbutton> <textbutton Text="Leave" BackgroundColor3={Color3.fromRGB(236, 0, 63)} TextColor3={Color3.fromRGB(255, 255, 255)} TextSize={14} Size={UDim2.fromOffset(112, 40)} FontFace={new Font("rbxasset://fonts/families/SourceSansPro.json", Enum.FontWeight.SemiBold)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0, 6)}/></textbutton> </frame>)}</__VelaBoundary.Consume>;}A button is a textbutton with sizing, a radius, a background and text styling. The difference
between the filled and outlined variants is one border border-slate-600 pair, which lowers to a
UIStroke child.
Hover feedback comes straight from the hover: variant,
and pairing it with transition tweens the change instead of snapping it:
<textbutton className="w-28 h-10 rounded-md bg-sky-500 hover:bg-sky-600 transition duration-150 text-white text-sm font-semibold" Text="Play"/>A variant moves the element onto the runtime path, and a gamepad or touch player may never hover — treat it as feedback, not the only signal.
There is no active: or pressed: variant. Press feedback is state you own. Track it with
React events and branch between two fully static class strings. Both looks stay on the static path
with the full utility set:
export function ActionButton(props: { label: string; onClick: () => void }) { const [pressed, setPressed] = React.useState(false);
return pressed ? ( <textbutton className="w-28 h-10 rounded-md bg-sky-600 text-white text-sm font-semibold" Text={props.label} Event={{ Activated: props.onClick, MouseButton1Up: () => setPressed(false), MouseButton1Down: () => setPressed(true), }} /> ) : ( <textbutton className="w-28 h-10 rounded-md bg-sky-500 text-white text-sm font-semibold" Text={props.label} Event={{ Activated: props.onClick, MouseButton1Up: () => setPressed(false), MouseButton1Down: () => setPressed(true), }} /> );}The duplication is the point: one computed string would push the element onto the runtime path, which resolves the same utilities but checks none of them.
Buttons do not size to their label. A bare textbutton is zero-sized. w-fit/h-fit lower to
AutomaticSize, but a fixed w-28 h-10 is the predictable default for anything sitting in a row
with other buttons.
Badges
import React from "@rbxts/react";
export function Badges() { return ( <frame className="flex gap-2 justify-center items-center w-96 h-16 p-3 rounded-lg bg-slate-900"> <textlabel className="w-20 h-6 rounded-full bg-emerald-950 border border-emerald-800 text-emerald-400 text-xs" Text="Online" /> <textlabel className="w-20 h-6 rounded-full bg-amber-950 border border-amber-800 text-amber-400 text-xs" Text="In match" /> <textlabel className="w-20 h-6 rounded-full bg-slate-800 border border-slate-700 text-slate-400 text-xs" Text="Offline" /> <textlabel className="w-20 h-6 rounded-full bg-rose-950 border border-rose-800 text-rose-400 text-xs" Text="Banned" /> </frame> );}import { __VelaBoundary } from "@rbxts/vela-runtime";import React from "@rbxts/react";export function Badges() { return <__VelaBoundary.Consume>{(<frame BackgroundColor3={Color3.fromRGB(15, 23, 43)} Size={UDim2.fromOffset(384, 64)} BorderSizePixel={0}><uilistlayout FillDirection={Enum.FillDirection.Horizontal} Padding={new UDim(0, 8)} HorizontalAlignment={Enum.HorizontalAlignment.Center} VerticalAlignment={Enum.VerticalAlignment.Center} SortOrder={Enum.SortOrder.LayoutOrder}/><uipadding PaddingTop={new UDim(0, 12)} PaddingRight={new UDim(0, 12)} PaddingBottom={new UDim(0, 12)} PaddingLeft={new UDim(0, 12)}/><uicorner CornerRadius={new UDim(0, 8)}/> <textlabel Text="Online" BackgroundColor3={Color3.fromRGB(0, 44, 34)} TextColor3={Color3.fromRGB(0, 212, 146)} TextSize={12} Size={UDim2.fromOffset(80, 24)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0.5, 0)}/><uistroke Thickness={1} Color={Color3.fromRGB(0, 96, 69)} Transparency={0}/></textlabel> <textlabel Text="In match" BackgroundColor3={Color3.fromRGB(70, 25, 1)} TextColor3={Color3.fromRGB(255, 185, 0)} TextSize={12} Size={UDim2.fromOffset(80, 24)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0.5, 0)}/><uistroke Thickness={1} Color={Color3.fromRGB(151, 60, 0)} Transparency={0}/></textlabel> <textlabel Text="Offline" BackgroundColor3={Color3.fromRGB(29, 41, 61)} TextColor3={Color3.fromRGB(144, 161, 185)} TextSize={12} Size={UDim2.fromOffset(80, 24)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0.5, 0)}/><uistroke Thickness={1} Color={Color3.fromRGB(49, 65, 88)} Transparency={0}/></textlabel> <textlabel Text="Banned" BackgroundColor3={Color3.fromRGB(77, 2, 24)} TextColor3={Color3.fromRGB(255, 99, 126)} TextSize={12} Size={UDim2.fromOffset(80, 24)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0.5, 0)}/><uistroke Thickness={1} Color={Color3.fromRGB(165, 0, 54)} Transparency={0}/></textlabel> </frame>)}</__VelaBoundary.Consume>;}A pill is a textlabel with rounded-full — new UDim(0.5, 0), half the instance’s own height, so
it stays a capsule at any size. Roblox centres label text by default, so no alignment classes are
needed, and the label’s own background paints the pill.
The colour pattern is worth stealing: fill from the dark end of a palette, border one step lighter,
text from the light end. Every built-in palette carries the same eleven shades, so it transfers to
any hue — swap emerald for amber, rose, or a palette of your own.
Stat bars
import React from "@rbxts/react";
export function StatBars() { return ( <frame className="flex flex-col gap-3 w-96 h-20 p-4 rounded-lg bg-slate-900"> <frame className="flex items-center gap-3 w-full h-4"> <textlabel className="w-10 h-4 text-left text-slate-400 text-xs font-semibold" Text="HP" /> <frame className="w-72 h-2 rounded-full bg-slate-800"> <frame className="w-2/3 h-full rounded-full bg-emerald-500" /> </frame> </frame>
<frame className="flex items-center gap-3 w-full h-4"> <textlabel className="w-10 h-4 text-left text-slate-400 text-xs font-semibold" Text="MP" /> <frame className="w-72 h-2 rounded-full bg-slate-800"> <frame className="w-1/4 h-full rounded-full bg-sky-500" /> </frame> </frame> </frame> );}import { __VelaBoundary } from "@rbxts/vela-runtime";import React from "@rbxts/react";export function StatBars() { return <__VelaBoundary.Consume>{(<frame BackgroundColor3={Color3.fromRGB(15, 23, 43)} Size={UDim2.fromOffset(384, 80)} 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, 8)}/> <frame Size={new UDim2(1, 0, 0, 16)} BorderSizePixel={0} BackgroundTransparency={1}><uilistlayout FillDirection={Enum.FillDirection.Horizontal} VerticalAlignment={Enum.VerticalAlignment.Center} Padding={new UDim(0, 12)} SortOrder={Enum.SortOrder.LayoutOrder}/> <textlabel Text="HP" TextXAlignment={Enum.TextXAlignment.Left} TextColor3={Color3.fromRGB(144, 161, 185)} TextSize={12} Size={UDim2.fromOffset(40, 16)} FontFace={new Font("rbxasset://fonts/families/SourceSansPro.json", Enum.FontWeight.SemiBold)} BorderSizePixel={0} BackgroundTransparency={1}/> <frame BackgroundColor3={Color3.fromRGB(29, 41, 61)} Size={UDim2.fromOffset(288, 8)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0.5, 0)}/> <frame BackgroundColor3={Color3.fromRGB(0, 188, 125)} Size={UDim2.fromScale(0.6666666667, 1)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0.5, 0)}/></frame> </frame> </frame>
<frame Size={new UDim2(1, 0, 0, 16)} BorderSizePixel={0} BackgroundTransparency={1}><uilistlayout FillDirection={Enum.FillDirection.Horizontal} VerticalAlignment={Enum.VerticalAlignment.Center} Padding={new UDim(0, 12)} SortOrder={Enum.SortOrder.LayoutOrder}/> <textlabel Text="MP" TextXAlignment={Enum.TextXAlignment.Left} TextColor3={Color3.fromRGB(144, 161, 185)} TextSize={12} Size={UDim2.fromOffset(40, 16)} FontFace={new Font("rbxasset://fonts/families/SourceSansPro.json", Enum.FontWeight.SemiBold)} BorderSizePixel={0} BackgroundTransparency={1}/> <frame BackgroundColor3={Color3.fromRGB(29, 41, 61)} Size={UDim2.fromOffset(288, 8)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0.5, 0)}/> <frame BackgroundColor3={Color3.fromRGB(0, 166, 244)} Size={UDim2.fromScale(0.25, 1)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0.5, 0)}/></frame> </frame> </frame> </frame>)}</__VelaBoundary.Consume>;}A meter needs no layout instance. The track is a rounded-full frame, and the fill a plain child
with w-2/3 h-full. With no UIListLayout on the track, the fill sits at its top-left origin.
Fractions lower to the scale component of Size, so w-2/3 means two thirds of the track with no
pixel maths.
To drive the fill from live data, resist the computed class string. w-* is in the runtime
resolver’s subset, but you would pay for the whole
runtime path to express a number. Set Size directly and
let the classes carry what does not change:
<frame className="w-72 h-2 rounded-full bg-slate-800"> <frame className="rounded-full bg-emerald-500" Size={UDim2.fromScale(health, 1)} /></frame>One rule makes this safe. Never set the same property from a class and a prop on one element. On
a collision Vela emits after you, and the class wins. An h-full beside that Size prop would
overwrite it with a Size = (0, 1) scale.
List rows
import React from "@rbxts/react";
export function Leaderboard() { return ( <frame className="flex flex-col gap-2 w-96 h-37 p-3 rounded-lg bg-slate-900 border border-slate-800"> <frame className="flex items-center gap-3 w-full h-9 px-3 rounded-md bg-slate-800"> <textlabel className="w-5 h-5 text-amber-400 text-sm font-semibold" Text="1" /> <frame className="w-6 h-6 rounded-full bg-gradient-to-br from-sky-400 to-indigo-500" /> <textlabel className="w-48 h-5 text-left text-slate-100 text-sm" Text="astra" /> <textlabel className="w-16 h-5 text-right text-amber-400 text-sm font-semibold" Text="2,410" /> </frame>
<frame className="flex items-center gap-3 w-full h-9 px-3 rounded-md bg-slate-800"> <textlabel className="w-5 h-5 text-slate-500 text-sm font-semibold" Text="2" /> <frame className="w-6 h-6 rounded-full bg-gradient-to-br from-emerald-400 to-teal-500" /> <textlabel className="w-48 h-5 text-left text-slate-100 text-sm" Text="void_walker" /> <textlabel className="w-16 h-5 text-right text-slate-300 text-sm" Text="1,984" /> </frame>
<frame className="flex items-center gap-3 w-full h-9 px-3 rounded-md bg-slate-800"> <textlabel className="w-5 h-5 text-slate-500 text-sm font-semibold" Text="3" /> <frame className="w-6 h-6 rounded-full bg-gradient-to-br from-rose-400 to-orange-500" /> <textlabel className="w-48 h-5 text-left text-slate-100 text-sm" Text="bloom" /> <textlabel className="w-16 h-5 text-right text-slate-300 text-sm" Text="1,730" /> </frame> </frame> );}import { __VelaBoundary } from "@rbxts/vela-runtime";import React from "@rbxts/react";export function Leaderboard() { return <__VelaBoundary.Consume>{(<frame BackgroundColor3={Color3.fromRGB(15, 23, 43)} Size={UDim2.fromOffset(384, 148)} BorderSizePixel={0}><uilistlayout FillDirection={Enum.FillDirection.Vertical} Padding={new UDim(0, 8)} SortOrder={Enum.SortOrder.LayoutOrder}/><uipadding PaddingTop={new UDim(0, 12)} PaddingRight={new UDim(0, 12)} PaddingBottom={new UDim(0, 12)} PaddingLeft={new UDim(0, 12)}/><uicorner CornerRadius={new UDim(0, 8)}/><uistroke Thickness={1} Color={Color3.fromRGB(29, 41, 61)} Transparency={0}/> <frame BackgroundColor3={Color3.fromRGB(29, 41, 61)} Size={new UDim2(1, 0, 0, 36)} BorderSizePixel={0}><uilistlayout FillDirection={Enum.FillDirection.Horizontal} VerticalAlignment={Enum.VerticalAlignment.Center} Padding={new UDim(0, 12)} SortOrder={Enum.SortOrder.LayoutOrder}/><uipadding PaddingLeft={new UDim(0, 12)} PaddingRight={new UDim(0, 12)}/><uicorner CornerRadius={new UDim(0, 6)}/> <textlabel Text="1" TextColor3={Color3.fromRGB(255, 185, 0)} TextSize={14} Size={UDim2.fromOffset(20, 20)} FontFace={new Font("rbxasset://fonts/families/SourceSansPro.json", Enum.FontWeight.SemiBold)} BorderSizePixel={0} BackgroundTransparency={1}/> <frame Size={UDim2.fromOffset(24, 24)} BackgroundColor3={Color3.fromRGB(255, 255, 255)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0.5, 0)}/><uigradient Color={new ColorSequence(Color3.fromRGB(0, 188, 255), Color3.fromRGB(97, 95, 255))} Rotation={45}/></frame> <textlabel Text="astra" TextXAlignment={Enum.TextXAlignment.Left} TextColor3={Color3.fromRGB(241, 245, 249)} TextSize={14} Size={UDim2.fromOffset(192, 20)} BorderSizePixel={0} BackgroundTransparency={1}/> <textlabel Text="2,410" TextXAlignment={Enum.TextXAlignment.Right} TextColor3={Color3.fromRGB(255, 185, 0)} TextSize={14} Size={UDim2.fromOffset(64, 20)} FontFace={new Font("rbxasset://fonts/families/SourceSansPro.json", Enum.FontWeight.SemiBold)} BorderSizePixel={0} BackgroundTransparency={1}/> </frame>
<frame BackgroundColor3={Color3.fromRGB(29, 41, 61)} Size={new UDim2(1, 0, 0, 36)} BorderSizePixel={0}><uilistlayout FillDirection={Enum.FillDirection.Horizontal} VerticalAlignment={Enum.VerticalAlignment.Center} Padding={new UDim(0, 12)} SortOrder={Enum.SortOrder.LayoutOrder}/><uipadding PaddingLeft={new UDim(0, 12)} PaddingRight={new UDim(0, 12)}/><uicorner CornerRadius={new UDim(0, 6)}/> <textlabel Text="2" TextColor3={Color3.fromRGB(98, 116, 142)} TextSize={14} Size={UDim2.fromOffset(20, 20)} FontFace={new Font("rbxasset://fonts/families/SourceSansPro.json", Enum.FontWeight.SemiBold)} BorderSizePixel={0} BackgroundTransparency={1}/> <frame Size={UDim2.fromOffset(24, 24)} 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))} Rotation={45}/></frame> <textlabel Text="void_walker" TextXAlignment={Enum.TextXAlignment.Left} TextColor3={Color3.fromRGB(241, 245, 249)} TextSize={14} Size={UDim2.fromOffset(192, 20)} BorderSizePixel={0} BackgroundTransparency={1}/> <textlabel Text="1,984" TextXAlignment={Enum.TextXAlignment.Right} TextColor3={Color3.fromRGB(202, 213, 226)} TextSize={14} Size={UDim2.fromOffset(64, 20)} BorderSizePixel={0} BackgroundTransparency={1}/> </frame>
<frame BackgroundColor3={Color3.fromRGB(29, 41, 61)} Size={new UDim2(1, 0, 0, 36)} BorderSizePixel={0}><uilistlayout FillDirection={Enum.FillDirection.Horizontal} VerticalAlignment={Enum.VerticalAlignment.Center} Padding={new UDim(0, 12)} SortOrder={Enum.SortOrder.LayoutOrder}/><uipadding PaddingLeft={new UDim(0, 12)} PaddingRight={new UDim(0, 12)}/><uicorner CornerRadius={new UDim(0, 6)}/> <textlabel Text="3" TextColor3={Color3.fromRGB(98, 116, 142)} TextSize={14} Size={UDim2.fromOffset(20, 20)} FontFace={new Font("rbxasset://fonts/families/SourceSansPro.json", Enum.FontWeight.SemiBold)} BorderSizePixel={0} BackgroundTransparency={1}/> <frame Size={UDim2.fromOffset(24, 24)} BackgroundColor3={Color3.fromRGB(255, 255, 255)} BorderSizePixel={0}><uicorner CornerRadius={new UDim(0.5, 0)}/><uigradient Color={new ColorSequence(Color3.fromRGB(255, 99, 126), Color3.fromRGB(255, 105, 0))} Rotation={45}/></frame> <textlabel Text="bloom" TextXAlignment={Enum.TextXAlignment.Left} TextColor3={Color3.fromRGB(241, 245, 249)} TextSize={14} Size={UDim2.fromOffset(192, 20)} BorderSizePixel={0} BackgroundTransparency={1}/> <textlabel Text="1,730" TextXAlignment={Enum.TextXAlignment.Right} TextColor3={Color3.fromRGB(202, 213, 226)} TextSize={14} Size={UDim2.fromOffset(64, 20)} BorderSizePixel={0} BackgroundTransparency={1}/> </frame> </frame>)}</__VelaBoundary.Consume>;}Lists are the pattern Roblox UIs live in: a flex flex-col gap-2 column whose children are
flex items-center gap-3 rows. Each element gets its own UIListLayout, so nesting costs nothing
to reason about.
The score column is pushed to the right edge with fixed widths. Rank, avatar, name and score plus
the gaps add up to the row’s inner width. justify-between exists and lowers to
UIListLayout.HorizontalFlex, but the renderer behind these previews does not implement flex
distribution. Both are legitimate in a real place, and the fixed-width one is what you need whenever
a column must not shrink.
In real code the rows come from data, and a className written as a literal inside the callback
is still a static string:
{entries.map((entry, index) => ( <frame className="flex items-center gap-3 w-full h-9 px-3 rounded-md bg-slate-800" key={entry.id}> {/* … */} </frame>))}What matters is the expression in the attribute, not where it appears. className={rowClasses} with
a computed rowClasses is dynamic. The literal above is not.
A confirmation dialog
import React from "@rbxts/react";
export function LeaveDialog() { return ( <frame className="flex flex-col gap-3 w-96 h-40 p-4 rounded-xl bg-slate-900 border border-slate-700"> <textlabel className="w-full h-6 text-left text-slate-100 text-lg font-semibold" Text="Leave match?" /> <textlabel className="w-full h-10 text-left text-slate-400 text-sm text-wrap" Text="You will lose your streak bonus and any unclaimed round rewards." /> <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="Cancel" /> <textbutton className="w-24 h-9 rounded-md bg-rose-600 text-white text-sm font-semibold" Text="Leave" /> </frame> </frame> );}import { __VelaBoundary } from "@rbxts/vela-runtime";import React from "@rbxts/react";export function LeaveDialog() { return <__VelaBoundary.Consume>{(<frame BackgroundColor3={Color3.fromRGB(15, 23, 43)} Size={UDim2.fromOffset(384, 160)} 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(49, 65, 88)} Transparency={0}/> <textlabel Text="Leave match?" TextXAlignment={Enum.TextXAlignment.Left} TextColor3={Color3.fromRGB(241, 245, 249)} TextSize={18} Size={new UDim2(1, 0, 0, 24)} FontFace={new Font("rbxasset://fonts/families/SourceSansPro.json", Enum.FontWeight.SemiBold)} BorderSizePixel={0} BackgroundTransparency={1}/> <textlabel Text="You will lose your streak bonus and any unclaimed round rewards." TextXAlignment={Enum.TextXAlignment.Left} TextColor3={Color3.fromRGB(144, 161, 185)} TextSize={14} TextWrapped={true} Size={new UDim2(1, 0, 0, 40)} BorderSizePixel={0} BackgroundTransparency={1}/> <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="Cancel" 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="Leave" BackgroundColor3={Color3.fromRGB(236, 0, 63)} TextColor3={Color3.fromRGB(255, 255, 255)} TextSize={14} Size={UDim2.fromOffset(96, 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>;}The dialog combines everything above. A column with gap-3, a text-wrap body, and a justify-end
row for the buttons that lowers to HorizontalAlignment = Right. TextWrapped is off by default,
so long text clips without it.
To present it as a modal, parent the card to a full-screen scrim and lift it above the rest of the interface:
<frame className="size-full bg-slate-950 opacity-60 z-50"> <frame className="origin-center left-1/2 top-1/2 w-96 h-40 …">{/* the card */}</frame></frame>Three details do the work. opacity-60 maps to BackgroundTransparency, inverted. origin-center
sets AnchorPoint to (0.5, 0.5), so left-1/2 top-1/2 centres the card rather than placing its
corner. And z-50 raises ZIndex. The scrim’s transparency does not cascade, so the card keeps its
own opaque background.
When a recipe needs state
Every pattern here eventually meets data: a selected tab, a disabled button, a filling meter. The rule is always the same:
- The look changes between known states → branch between two complete static literals, as the button does. Full utility set, full diagnostics, zero runtime cost.
- A number changes continuously → keep the classes static and set the property directly, as the meter does, on a property no class on that element touches.
- Only a colour, radius, spacing or size varies → a computed string is acceptable, since those families are within the runtime resolver’s subset. Know that you are opting into the runtime path for that element.
- The state is the pointer being over the element → that one is built in:
hover:, ideally withtransition.
What you should not do is compute a string carrying layout classes — flex, items-center,
alignment, text styling. Those are dropped silently at runtime, and the failure mode is a broken
screen with a clean build. See dynamic class names, or
the short version in the
five rules.
See also
- Your first component — the same walkthrough at single-utility resolution.
- Layout and sizing — the layout model these recipes lean on.
- Troubleshooting — when a recipe does not look like its preview.