# Text and fonts

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

Source: https://docs.astra-void.xyz/vela-rbxts/guides/text-and-fonts/

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`.

```tsx title="src/client/Title.tsx"
<textlabel
  className="text-2xl font-semibold text-slate-100 text-center align-middle"
  Text="Match found"
/>
```

_Interactive preview: Three labels: size, weight, color and alignment, all from the text-* and font-* families._

> **Weights are approximated in the preview**
>
> The **Lowered** tab shows what Vela really emits for `font-*`: a `FontFace` carrying a `Font` value built from the theme's family, the weight, and the style. The browser renderer behind these previews has no `Font` datatype, so the preview maps each weight onto the nearest legacy `Font` enum member instead. Relative weights read correctly. The exact face does not.

## 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.

> **Editor warns, build does not**
>
> The utility-per-host rules are enforced **only** by the editor and LSP. Text utilities belong on `textlabel`, `textbutton` and `textbox`, `image-*` on `imagelabel` and `imagebutton`, and `placeholder-*` on `textbox`. `rbxtsc` will happily emit `TextColor3` on a `Frame`.
>
> Roblox ignores an unknown property assignment, so nothing crashes — the class does nothing and you get no signal outside the editor. Use [editor setup](https://docs.astra-void.xyz/vela-rbxts/guides/editor-setup.md) to get the 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`.

```tsx title="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](https://docs.astra-void.xyz/vela-rbxts/getting-started/how-it-works.md#classname-on-components).

## 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](https://docs.astra-void.xyz/vela-rbxts/guides/theming.md#rem).

| Class | `TextSize` |
| --- | --- |
| `text-xs` | 12 |
| `text-sm` | 14 |
| `text-base` | 16 |
| `text-lg` | 18 |
| `text-xl` | 20 |
| `text-2xl` | 24 |
| `text-3xl` | 30 |
| `text-4xl` | 36 |
| `text-5xl` | 48 |
| `text-6xl` | 60 |
| `text-7xl` | 72 |
| `text-8xl` | 96 |
| `text-9xl` | 128 |

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

> **A scaled TextSize stops at 100**
>
> Roblox stops honouring `TextSize` past 100 and does it silently, so rem stops there too: on a large viewport `text-6xl` and up land on that ceiling.
>
> The ceiling belongs to the scaling, so it covers arbitrary values too: `text-[240px]` emits `__VelaRem.scaleText(240, 0)` and renders at 100. [Pin the clamp](https://docs.astra-void.xyz/vela-rbxts/guides/theming.md#pinning-offsets-back-to-literal-pixels) and the scaling leaves the emit entirely.

## 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`.

```tsx title="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:

```lua
new Font("rbxasset://fonts/families/SourceSansPro.json", Enum.FontWeight.SemiBold)
```

| Class | `Enum.FontWeight` |
| --- | --- |
| `font-thin` | `Thin` |
| `font-extralight` | `ExtraLight` |
| `font-light` | `Light` |
| `font-normal` | `Regular` |
| `font-medium` | `Medium` |
| `font-semibold` | `SemiBold` |
| `font-bold` | `Bold` |
| `font-extrabold` | `ExtraBold` |
| `font-black` | `Heavy` |

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:

| Class | Family |
| --- | --- |
| `font-sans` | Source Sans Pro — the default when no family class is present |
| `font-serif` | Merriweather |
| `font-mono` | Roboto Mono |

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

```ts title="vela.config.ts"
export default defineConfig({
  theme: {
    extend: {
      fontFamily: {
        display: "rbxassetid://12345678",
        body: "rbxasset://fonts/families/Nunito.json",
      },
    },
  },
});
```

```tsx
<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`**.

> **Setting `FontFace` directly still works**
>
> A font you do not want in the theme can be set as a prop, leaving `font-*` off that element:
>
> ```tsx
> <textlabel
>   FontFace={new Font("rbxasset://fonts/families/GothamSSm.json", Enum.FontWeight.Bold)}
>   Text="Custom family"
> />
> ```
>
> It does not compose with `font-bold` or `italic`, which emit a whole `FontFace` of their own.

## 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.

```tsx title="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**:

```tsx title="In"
<textlabel className="uppercase" Text="match found" />
```

```tsx title="Out"
<textlabel Text="MATCH FOUND" />
```

When `Text` is an expression, the transform has to run in-game. The element moves onto the
[runtime path](https://docs.astra-void.xyz/vela-rbxts/guides/dynamic-class-names.md), 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](https://docs.astra-void.xyz/vela-rbxts/guides/colors-and-surfaces.md) 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](https://docs.astra-void.xyz/vela-rbxts/guides/colors-and-surfaces.md) — the color half of `text-*`, plus `transparent` and the shade rules.
- [Theming](https://docs.astra-void.xyz/vela-rbxts/guides/theming.md) — where `text-{color}` keys come from.
- [Editor setup](https://docs.astra-void.xyz/vela-rbxts/guides/editor-setup.md) — getting the host-restriction warnings the compiler does not give you.
- [Diagnostics](https://docs.astra-void.xyz/vela-rbxts/reference/diagnostics.md) — every code and what triggers it.
