# Typography

The type scale, weights, and families Propel ships — and the utilities that apply them.

Propel ships one type scale, applied through composite utilities. A single class sets font size,
line height, letter spacing, and weight together, so a piece of text can't end up with the size of
one style and the leading of another.

```html
<p class="text-body-sm-medium">Save changes</p>
```

Every name is `text-<step>-<weight>`: pick the step by the job the text does, then the weight by how
much emphasis it needs. There are 57 styles across three tiers — headings for
section titles, body for text meant to be read, and captions for text that labels something.

Propel replaces Tailwind's type scale rather than adding to it. The theme resets `--text-*`,
`--font-*`, `--font-weight-*`, and `--tracking-*` to `initial` before declaring its own, so
Tailwind's defaults such as `text-sm`, `font-mono`, and `font-normal` generate no CSS at all and
fail silently, inheriting whatever was set further up the tree. The
[substitutions](#substitutions-for-tailwinds-defaults) below list what to use instead.

## Headings

Six steps from 32px down to 16px, all on a tight 1.2 leading (1.3 at h6). Pick the step by the level of the section, not by the size you want.

| Step | Utility | Size | Line height | Letter spacing | Weights | Used for |
| --- | --- | --- | --- | --- | --- | --- |
| Heading 1 | `text-h1-semibold` | `2rem / 32px` | `1.2` | `0em` | `regular`, `medium`, `semibold`, `bold` | One per page — the page title. Nothing above it. |
| Heading 2 | `text-h2-semibold` | `1.75rem / 28px` | `1.2` | `0em` | `regular`, `medium`, `semibold`, `bold` | A major section of a page. |
| Heading 3 | `text-h3-semibold` | `1.5rem / 24px` | `1.2` | `0em` | `regular`, `medium`, `semibold`, `bold` | A subsection. The largest step most product UI needs. |
| Heading 4 | `text-h4-semibold` | `1.25rem / 20px` | `1.2` | `0em` | `regular`, `medium`, `semibold`, `bold` | Dialog and drawer titles, panel headers. |
| Heading 5 | `text-h5-semibold` | `1.125rem / 18px` | `1.2` | `0em` | `regular`, `medium`, `semibold`, `bold` | A heading inside an already-dense region. |
| Heading 6 | `text-h6-semibold` | `1rem / 16px` | `1.3` | `0em` | `regular`, `medium`, `semibold`, `bold` | Card and group titles. Leading opens to 1.3 — it sits close to body size. |

## Body

Four steps on a roomy 1.54 leading, tuned for reading more than one line. Body SM is the workhorse of product UI.

| Step | Utility | Size | Line height | Letter spacing | Weights | Used for |
| --- | --- | --- | --- | --- | --- | --- |
| Body MD | `text-body-md-regular` | `1rem / 16px` | `1.54` | `0em` | `regular`, `medium`, `semibold`, `bold` | Long-form prose and page intros. Cap the measure at 65–75 characters. |
| Body SM | `text-body-sm-regular` | `0.875rem / 14px` | `1.54` | `0em` | `regular`, `medium`, `semibold`, `bold` | The default for interface text — controls, menu rows, table cells, descriptions. Reach here first. |
| Body XS | `text-body-xs-regular` | `0.8125rem / 13px` | `1.54` | `0em` | `regular`, `medium`, `semibold`, `bold` | Dense interface text that still runs to multiple lines — sidebars, compact lists. |
| Body 2XS | `text-body-2xs-regular` | `0.75rem / 12px` | `1.54` | `0em` | `regular`, `medium`, `semibold` | The smallest text that still wraps — a banner or toast description under a 13px title. Same size as Caption MD, but on the body leading; a single-line label belongs in the caption tier. |

## Captions

Five steps for text that labels something rather than being read as prose. Four sit on a 1.2 leading, with XS and 2XS opening their tracking to 0.01em to stay legible; 3XS drops to 8px and opens its leading to 1.4 instead.

| Step | Utility | Size | Line height | Letter spacing | Weights | Used for |
| --- | --- | --- | --- | --- | --- | --- |
| Caption MD | `text-caption-md-medium` | `0.75rem / 12px` | `1.2` | `0em` | `regular`, `medium`, `semibold`, `bold` | Field labels, badge text, section eyebrows, metadata rows. |
| Caption SM | `text-caption-sm-medium` | `0.6875rem / 11px` | `1.2` | `0em` | `regular`, `medium`, `semibold`, `bold` | Secondary metadata and helper text under a control. |
| Caption XS | `text-caption-xs-medium` | `0.625rem / 10px` | `1.2` | `0.01em` | `regular`, `medium`, `semibold`, `bold` | Counters and chips where space is genuinely scarce. |
| Caption 2XS | `text-caption-2xs-medium` | `0.5625rem / 9px` | `1.2` | `0.01em` | `regular`, `medium`, `semibold` | Numeric badges and counters only. Never a sentence, and never the only carrier of a meaning. |
| Caption 3XS | `text-caption-3xs-medium` | `0.5rem / 8px` | `1.4` | `0em` | `regular`, `medium`, `semibold` | Below the floor — a one- or two-character mark inside a 16px avatar or icon. Never a word, and never the only carrier of a meaning. |

## Weights

Text styles use four of the six declared weights. Propel's regular is 450 rather than the usual 400,
because Inter Variable reads slightly light at 400 for interface text, so the whole scale sits half
a step up.

`light` and `heavy` are declared as primitives but no text style uses them; reach for one only when
a composite genuinely can't express what you need.

| Utility | Value | Styles | Used for |
| --- | --- | --- | --- |
| `font-light` | `350` | `none` | — |
| `font-regular` | `450` | `15` | Body copy and long-form prose. Propel's true body weight. |
| `font-medium` | `500` | `15` | UI labels, buttons, table headers — emphasis without a heading. |
| `font-semibold` | `600` | `15` | Headings and section titles. |
| `font-bold` | `700` | `12` | The strongest emphasis a text style carries. |
| `font-heavy` | `800` | `none` | — |

## Font families

Two faces do all the work: Inter for the interface, IBM Plex Mono for anything read character by
character. Propel names them but bundles no font files — see
[Installation](/installation#load-the-fonts) for self-hosting, or let the stacks fall back to the
system fonts.

| Utility | Face | Used for |
| --- | --- | --- |
| `font-body` | `Inter Variable` | Everything that isn't code. The default — set once on the page, rarely restated. |
| `font-heading` | `Inter Variable` | Headings. Declared separately so the two can diverge later; today it resolves to the same stack as font-body. |
| `font-code` | `IBM Plex Mono` | Code, API identifiers, and any value meant to be read character by character. |

The composite styles set size, leading, tracking, and weight — never the family. Set the family
once at the root and let it inherit; `font-code` is the only one worth repeating locally.

## Every text style

The complete index. Sizes are shown at the 16px root the scale is authored against.

| Utility | Size | Line height | Weight | Letter spacing |
| --- | --- | --- | --- | --- |
| `text-h1-regular` | `2rem / 32px` | `1.2` | `450` | `0em` |
| `text-h1-medium` | `2rem / 32px` | `1.2` | `500` | `0em` |
| `text-h1-semibold` | `2rem / 32px` | `1.2` | `600` | `0em` |
| `text-h1-bold` | `2rem / 32px` | `1.2` | `700` | `0em` |
| `text-h2-regular` | `1.75rem / 28px` | `1.2` | `450` | `0em` |
| `text-h2-medium` | `1.75rem / 28px` | `1.2` | `500` | `0em` |
| `text-h2-semibold` | `1.75rem / 28px` | `1.2` | `600` | `0em` |
| `text-h2-bold` | `1.75rem / 28px` | `1.2` | `700` | `0em` |
| `text-h3-regular` | `1.5rem / 24px` | `1.2` | `450` | `0em` |
| `text-h3-medium` | `1.5rem / 24px` | `1.2` | `500` | `0em` |
| `text-h3-semibold` | `1.5rem / 24px` | `1.2` | `600` | `0em` |
| `text-h3-bold` | `1.5rem / 24px` | `1.2` | `700` | `0em` |
| `text-h4-regular` | `1.25rem / 20px` | `1.2` | `450` | `0em` |
| `text-h4-medium` | `1.25rem / 20px` | `1.2` | `500` | `0em` |
| `text-h4-semibold` | `1.25rem / 20px` | `1.2` | `600` | `0em` |
| `text-h4-bold` | `1.25rem / 20px` | `1.2` | `700` | `0em` |
| `text-h5-regular` | `1.125rem / 18px` | `1.2` | `450` | `0em` |
| `text-h5-medium` | `1.125rem / 18px` | `1.2` | `500` | `0em` |
| `text-h5-semibold` | `1.125rem / 18px` | `1.2` | `600` | `0em` |
| `text-h5-bold` | `1.125rem / 18px` | `1.2` | `700` | `0em` |
| `text-h6-regular` | `1rem / 16px` | `1.3` | `450` | `0em` |
| `text-h6-medium` | `1rem / 16px` | `1.3` | `500` | `0em` |
| `text-h6-semibold` | `1rem / 16px` | `1.3` | `600` | `0em` |
| `text-h6-bold` | `1rem / 16px` | `1.3` | `700` | `0em` |
| `text-body-md-regular` | `1rem / 16px` | `1.54` | `450` | `0em` |
| `text-body-md-medium` | `1rem / 16px` | `1.54` | `500` | `0em` |
| `text-body-md-semibold` | `1rem / 16px` | `1.54` | `600` | `0em` |
| `text-body-md-bold` | `1rem / 16px` | `1.54` | `700` | `0em` |
| `text-body-sm-regular` | `0.875rem / 14px` | `1.54` | `450` | `0em` |
| `text-body-sm-medium` | `0.875rem / 14px` | `1.54` | `500` | `0em` |
| `text-body-sm-semibold` | `0.875rem / 14px` | `1.54` | `600` | `0em` |
| `text-body-sm-bold` | `0.875rem / 14px` | `1.54` | `700` | `0em` |
| `text-body-xs-regular` | `0.8125rem / 13px` | `1.54` | `450` | `0em` |
| `text-body-xs-medium` | `0.8125rem / 13px` | `1.54` | `500` | `0em` |
| `text-body-xs-semibold` | `0.8125rem / 13px` | `1.54` | `600` | `0em` |
| `text-body-xs-bold` | `0.8125rem / 13px` | `1.54` | `700` | `0em` |
| `text-body-2xs-regular` | `0.75rem / 12px` | `1.54` | `450` | `0em` |
| `text-body-2xs-medium` | `0.75rem / 12px` | `1.54` | `500` | `0em` |
| `text-body-2xs-semibold` | `0.75rem / 12px` | `1.54` | `600` | `0em` |
| `text-caption-md-regular` | `0.75rem / 12px` | `1.2` | `450` | `0em` |
| `text-caption-md-medium` | `0.75rem / 12px` | `1.2` | `500` | `0em` |
| `text-caption-md-semibold` | `0.75rem / 12px` | `1.2` | `600` | `0em` |
| `text-caption-md-bold` | `0.75rem / 12px` | `1.2` | `700` | `0em` |
| `text-caption-sm-regular` | `0.6875rem / 11px` | `1.2` | `450` | `0em` |
| `text-caption-sm-medium` | `0.6875rem / 11px` | `1.2` | `500` | `0em` |
| `text-caption-sm-semibold` | `0.6875rem / 11px` | `1.2` | `600` | `0em` |
| `text-caption-sm-bold` | `0.6875rem / 11px` | `1.2` | `700` | `0em` |
| `text-caption-xs-regular` | `0.625rem / 10px` | `1.2` | `450` | `0.01em` |
| `text-caption-xs-medium` | `0.625rem / 10px` | `1.2` | `500` | `0.01em` |
| `text-caption-xs-semibold` | `0.625rem / 10px` | `1.2` | `600` | `0.01em` |
| `text-caption-xs-bold` | `0.625rem / 10px` | `1.2` | `700` | `0.01em` |
| `text-caption-2xs-regular` | `0.5625rem / 9px` | `1.2` | `450` | `0.01em` |
| `text-caption-2xs-medium` | `0.5625rem / 9px` | `1.2` | `500` | `0.01em` |
| `text-caption-2xs-semibold` | `0.5625rem / 9px` | `1.2` | `600` | `0.01em` |
| `text-caption-3xs-regular` | `0.5rem / 8px` | `1.4` | `450` | `0em` |
| `text-caption-3xs-medium` | `0.5rem / 8px` | `1.4` | `500` | `0em` |
| `text-caption-3xs-semibold` | `0.5rem / 8px` | `1.4` | `600` | `0em` |

## Font size

The composites are built from a numeric size scale, named for the pixel value at a 16px root, so
`text-14` is `0.875rem`. It is the primitive layer, not a shortcut: a step sets `font-size` alone,
so text using one opts out of the leading, tracking and weight a real text style carries and
inherits whatever is around it instead. Reach for a composite every time — `propel/no-raw-font-size`
makes a bare step an error in the library, and the same rule runs over this site.

| Utility | rem | px | Note |
| --- | --- | --- | --- |
| `text-8` | `0.5rem` | `8px` |  |
| `text-9` | `0.5625rem` | `9px` |  |
| `text-10` | `0.625rem` | `10px` |  |
| `text-11` | `0.6875rem` | `11px` |  |
| `text-12` | `0.75rem` | `12px` |  |
| `text-13` | `0.8125rem` | `13px` |  |
| `text-14` | `0.875rem` | `14px` |  |
| `text-15` | `0.9375rem` | `15px` | Declared, but no text style uses this step. |
| `text-16` | `1rem` | `16px` |  |
| `text-17` | `1.0625rem` | `17px` | Declared, but no text style uses this step. |
| `text-18` | `1.125rem` | `18px` |  |
| `text-19` | `1.1875rem` | `19px` | Declared, but no text style uses this step. |
| `text-20` | `1.25rem` | `20px` |  |
| `text-21` | `1.3125rem` | `21px` | Declared, but no text style uses this step. |
| `text-24` | `1.5rem` | `24px` |  |
| `text-27` | `1.6875rem` | `27px` | Declared, but no text style uses this step. |
| `text-28` | `1.75rem` | `28px` |  |
| `text-32` | `2rem` | `32px` |  |
| `text-40` | `2.5rem` | `40px` | Declared, but no text style uses this step. |

## Letter spacing

Tracking is deliberately near zero, since Inter needs no dramatic tightening at interface sizes.
Every text style uses `tracking-default` except the 10px and 9px captions, which open to a literal
`0.01em`. The 8px step closes back to default and opens its leading to 1.4 instead.

| Utility | Value | Styles |
| --- | --- | --- |
| `tracking-extra-tight` | `-0.004em` | `none` |
| `tracking-tight` | `-0.002em` | `none` |
| `tracking-default` | `0em` | `50` |
| `tracking-wide` | `0.002em` | `none` |

## Substitutions for Tailwind's defaults

Propel's reset removes these; each has a direct replacement.

| Tailwind default | Propel equivalent | Note |
| --- | --- | --- |
| `text-xs … text-9xl` | `text-body-sm-regular, text-14` | The whole t-shirt scale is removed. Use a composite style, or a numeric step for a one-off. |
| `font-sans` | `font-body` | font-heading is declared separately and currently resolves to the same stack. |
| `font-mono` | `font-code` |  |
| `font-normal` | `font-regular` | Propel's regular is 450, not 400. font-thin, font-extrabold, and font-black are also gone. |
| `tracking-normal` | `tracking-default` | tracking-tight survives the reset but is redeclared at -0.002em, not Tailwind's -0.025em. |

`leading-*` is the exception. Propel doesn't reset it, so Tailwind's line-height utilities still
work, though they are rarely needed since every text style brings its own.

## Guidelines

A heading token does not set the element. Choose `<h1>` through `<h6>` from the document's real
structure, because screen reader users navigate by heading level, then apply whichever step the
design calls for. A card title inside a page section is often `<h3 class="text-h6-semibold">`, and
that is correct.

Don't restate what a composite already sets. Stacking `leading-*`, `font-*`, or `tracking-*` on top
of `text-body-md-regular` fights the pairing the token exists to guarantee, and which declaration
wins comes down to Tailwind's utility ordering rather than the order the classes are written. A
design needing a different combination needs a different step.

Cap long-form text at 65–75 characters per line and leave it at `text-body-md-regular`. The 1.54
leading on the body tier is what makes multi-line text comfortable, while the caption tier's 1.2
leading is for single lines, so a paragraph set in a caption style reads as cramped.

Text below `text-caption-md-*` is for labels and counters rather than content a reader must not
miss. Pair the 8px, 9px and 10px steps with color, icon, or position instead of letting size carry
the meaning alone.
