Velaguides

Text and fonts

Text sizing, weight, alignment, wrapping and truncation — plus the host restriction the compiler does not actually enforce.

Text utilities write to the text properties of Roblox’s text instances: TextSize, TextColor3, FontFace, TextXAlignment, TextYAlignment, TextWrapped, TextTruncate and LineHeight. Two of them rewrite the Text string itself. Three host elements have those properties: textlabel, textbutton, and textbox.

src/client/Title.tsx
<textlabel
className="text-2xl font-semibold text-slate-100 text-center align-middle"
Text="Match found"
/>
Three labels: size, weight, color and alignment, all from the text-* and font-* families.

The host restriction is editor-only

Vela knows which utilities belong on which host element, and reports unsupported-host-utility when you get it wrong. That check lives in the LSP surface — diagnostics, hover, completions, and document colors.

The compiler never consults it. The transform pipeline does not check host tags at all. <frame className="text-red-500" /> compiles cleanly and emits TextColor3 onto a Frame, with no build-time warning.

The restriction does not apply to components

Inside a component’s className the rule does not exist. Vela has no idea which host element <Title /> renders, so the editor completes and hovers the full set, text-*, image-* and placeholder-* included. It never reports unsupported-host-utility.

No warning anywhere, in the editor or the build
<Title className="text-2xl font-semibold" Text="Match found" />

Whether that text-2xl reaches a TextSize depends on your component forwarding what it does not consume down to a text host. See How it works.

How text-* is disambiguated

text- is the most overloaded prefix in the whole system. One rule, applied in order, decides what a given text-* class means:

  1. If the payload is a text-size key (xs through 9xl), it is TextSize.
  2. If the payload is left, center, right, or justify, it is TextXAlignment.
  3. If the payload is wrap or nowrap, it is TextWrap.
  4. Everything else falls through to TextColor.

The fourth step is a catch-all, so there is no such thing as an unrecognised text-* class. text-foo is read as a colour, foo is not in theme.colors, and you get unknown-theme-key rather than unsupported-utility-family.

So read unknown-theme-key on a text-* class as “you probably meant a size or an alignment”.

Size

text-{size} sets TextSize, in rem — the numbers below are what each key is worth at the base viewport. See rem.

ClassTextSize
text-xs12
text-sm14
text-base16
text-lg18
text-xl20
text-2xl24
text-3xl30
text-4xl36
text-5xl48
text-6xl60
text-7xl72
text-8xl96
text-9xl128

The set is fixed and does not come from the theme. Anything outside the list falls through to the colour branch above.

Weight, family and style

font-* is one prefix over three axes. It resolves the fixed weight names first and reads anything else as a font family key, so weight, family and style merge into a single FontFace.

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

Weight

font-{weight} sets the weight axis of that Font value:

new Font("rbxasset://fonts/families/SourceSansPro.json", Enum.FontWeight.SemiBold)
ClassEnum.FontWeight
font-thinThin
font-extralightExtraLight
font-lightLight
font-normalRegular
font-mediumMedium
font-semiboldSemiBold
font-boldBold
font-extraboldExtraBold
font-blackHeavy

Two names do not match their class: font-normal produces Regular and font-black produces Heavy, following the Roblox enum member names.

italic rides in the same FontFace value as a third argument, so font-bold italic emits new Font("…/SourceSansPro.json", Enum.FontWeight.Bold, Enum.FontStyle.Italic). not-italic resets the style axis.

Family

The family is a theme axis: theme.fontFamily ships three keys, and font-{key} selects one:

ClassFamily
font-sansSource Sans Pro — the default when no family class is present
font-serifMerriweather
font-monoRoboto Mono

Add your own the way you add any other theme key. The value is a Roblox font family asset path, not an expression:

vela.config.ts
export default defineConfig({
theme: {
extend: {
fontFamily: {
display: "rbxassetid://12345678",
body: "rbxasset://fonts/families/Nunito.json",
},
},
},
});
<textlabel className="font-display font-bold" Text="Match found" />

Because the family lookup is the fallback branch, a payload that is neither a weight nor a configured family key reports unknown-theme-key.

Alignment

Horizontal alignment comes from text-left, text-center, and text-right, which set TextXAlignment.

Vertical alignment uses a different prefix: align-top, align-middle and align-bottom set TextYAlignment to Top, Center and Bottom. Anything else after align- reports unsupported-text-alignment.

text-justify parses but does not resolve, since TextXAlignment has no justified mode. It reports unsupported-text-alignment.

Line height

leading-{key} sets LineHeight from six named keys — none (1), tight (1.25), snug (1.375), normal (1.5), relaxed (1.625), loose (2). The numeric Tailwind forms (leading-5, leading-[1.2]) are not accepted and report unsupported-line-height-value.

Wrapping and truncation

text-wrap sets TextWrapped = true, and text-nowrap sets it to false. The whitespace-normal and whitespace-nowrap pair is an accepted alias for the same property. Any other whitespace-* value is unsupported-whitespace-value.

truncate sets TextTruncate = Enum.TextTruncate.AtEnd. It takes no payload, and AtEnd is the only truncation mode Vela emits.

src/client/PlayerRow.tsx
<textlabel className="text-sm text-nowrap truncate" Text={playerName} />

Case transforms rewrite the string

There is no text-transform property, so uppercase, lowercase, capitalize and normal-case work on the Text string itself. When Text is a literal the rewrite happens at compile time:

In
<textlabel className="uppercase" Text="match found" />
Out
<textlabel Text="MATCH FOUND" />

When Text is an expression, the transform has to run in-game. The element moves onto the runtime path, and the helper transforms the value as it changes.

Decorations use RichText

underline and line-through wrap the text in RichText markup, emitting Text="<u>hello</u>" with RichText={true} and escaping the content as needed. no-underline removes them. If the element sets RichText itself, Vela backs off with decoration-on-richtext. overline has no RichText equivalent and reports no-roblox-equivalent.

Placeholders

placeholder-* sets PlaceholderColor3 and is meaningful only on textbox. As with every host restriction on this page, the editor flags it and the compiler does not.

placeholder-transparent is an error (unsupported-color-key) — PlaceholderColor3 has no paired transparency property. See Colours and surfaces for the other families.

Typography families that cannot exist

A few Tailwind typography families have no Roblox property to target, and report no-roblox-equivalent rather than reading as typos:

  • tracking-* — letter spacing. The Roblox text engine exposes nothing for it.
  • indent-*, break-*, hyphens-*, list-* — no indentation, line-breaking, or list-marker control.
  • decoration-* and overline — RichText has underline and strikethrough only.

See also

  • Colors and surfaces — the color half of text-*, plus transparent and the shade rules.
  • Theming — where text-{color} keys come from.
  • Editor setup — getting the host-restriction warnings the compiler does not give you.
  • Diagnostics — every code and what triggers it.