Skip to content
Monogem

Typography

Overview

Typography defines how text looks and reads across the system: the font families, the size ramp, weights, line-heights, and letter-spacing. It commits to one typeface — Geist (with Geist Mono for code) — and keeps a native font stack as a fallback.

Every value maps 1:1 to Tailwind's text-* scale, so tokens and utilities never disagree. Following Tailwind and shadcn, the model is per-property tokens composed at the call site — there's no .text-h1 class. Common text roles live as a recipe table (see Text roles) that also maps to Figma text styles.

Two layers, same tiering as Scale and Color:

  • Primitives — the raw ramps (--font-size-*, --font-weight-*, --leading-*, --tracking-*) in tokens/primitives.css.
  • Semantic — the font families (--font-sans, --font-mono) in tokens/semantic.css. Families have no ramp, so they're defined directly here — re-point them to a brand face in one line.

Principles

  • Accessible by default. 16px (base) is the floor for reading text. Body line-height is pinned at 1.5 (WCAG 1.4.12). Weights below 400 are excluded. The 10px size is fenced to decorative use.
  • Semantic over literal. xs/lg/xl describe appearance, so they stay at the primitive tier. Intent lives in the recipe table and Figma styles.
  • Tailwind-aligned. Sizes, weights, leadings, and trackings all equal a Tailwind default utility — --font-size-xl is text-xl.
  • Restraint. Four weights, four leadings, three trackings; the size ramp stops at 5xl; no bespoke class API.

Tokens

Font families (semantic)

TokenStackUse for
--font-sansGeist → system UI stackAll UI text
--font-monoGeist Mono → system mono stackCode, tabular figures

Size ramp (primitives)

rem-based; px is a reading aid. Base = 16px.

TokenrempxTypical role
--font-size-2xs0.62510Overline — decorative only
--font-size-xs0.7512Captions, metadata
--font-size-sm0.87514UI labels, secondary text
--font-size-base116Body — the anchor
--font-size-lg1.12518Lead paragraph
--font-size-xl1.2520Small heading
--font-size-2xl1.524h3
--font-size-3xl1.87530h2
--font-size-4xl2.2536h1
--font-size-5xl348Display

Weights (primitives)

TokenValueRole
--font-weight-normal400Body / base
--font-weight-medium500Buttons, UI labels
--font-weight-semibold600Headings
--font-weight-bold700Displays, strong emphasis

Line-height (primitives)

Unitless, so the ratio scales with any size.

TokenValueUse for
--leading-tight1.2Displays & large headings
--leading-snug1.375Small headings, short UI text
--leading-normal1.5Body (default)
--leading-relaxed1.625Long-form reading

Letter-spacing (primitives)

em-based, so it scales with size.

TokenValueUse for
--tracking-tight-0.025emLarge headings
--tracking-normal0Everything (default)
--tracking-wide0.05em2xs uppercase overline

Text roles

Compose these in markup — this table is the reference, and maps 1:1 to Figma text styles.

RoleTailwind utilitiessize / weight / tracking / leading
Displaytext-5xl font-bold tracking-tight leading-tight48 / 700 / -0.025em / 1.2
Heading 1text-4xl font-semibold tracking-tight leading-tight36 / 600 / -0.025em / 1.2
Heading 2text-3xl font-semibold tracking-tight leading-snug30 / 600 / -0.025em / 1.375
Heading 3text-2xl font-semibold leading-snug24 / 600 / 0 / 1.375
Heading 4text-xl font-semibold leading-snug20 / 600 / 0 / 1.375
Leadtext-lg leading-relaxed18 / 400 / 0 / 1.625
Bodytext-base leading-normal16 / 400 / 0 / 1.5
Body smalltext-sm leading-normal14 / 400 / 0 / 1.5
Label / buttontext-sm font-medium leading-snug14 / 500 / 0 / 1.375
Captiontext-xs leading-snug12 / 400 / 0 / 1.375
Overlinetext-2xs font-medium uppercase tracking-wide10 / 500 / +0.05em
Codefont-mono text-smGeist Mono / 14

Rationale

Why commit to Geist (not a system stack)? A single typeface gives consistent rendering across operating systems, and Geist is clean, modern, and ships a matched monospace. The system stack stays as a fallback so text renders correctly before the webfont loads. --font-sans is still re-pointable — swap in a brand face in one line, like --primary.

Why a separate ramp, not the 4px master scale? Type sizes want to be multiplicative (each step proportional to the last) so hierarchy holds across screens; spacing wants to be linear (snap to a 4px grid). Different jobs, so type gets its own honest ladder. The master scale still owns all spacing around text.

Why 16px as the floor? Smaller text fails readability for primary content. 14px is for secondary UI only; 12px for captions; 10px (2xs) for decorative micro-labels — never body.

Why body line-height 1.5? WCAG 1.4.12 requires ≥1.5× for body text. Unitless keeps the ratio correct at every size (16px→24px, 18px→27px) and avoids a fixed value clipping on larger text.

Why per-property tokens, not .text-h1 classes? It's how Tailwind and shadcn work — hierarchy composes at the call site. A bespoke class API is a parallel surface Tailwind already makes unnecessary. If a recipe gets repeated everywhere, add a class then (not before).

Do / Don’t

Do

  • Use --font-size-base (16px) or larger for anything read as content.
  • Keep body at --leading-normal (1.5) — it's a WCAG requirement, not a preference.
  • Compose text roles from the recipe table; keep them in sync with Figma text styles.
  • Reach for --font-mono for code and tabular figures.

Don’t

  • Use 2xs (10px) for body, or for anything a user must actually read.
  • Hard-code font sizes (15px) — pick the nearest ramp step.
  • Invent a weight below 400 or above 700, or add a size past 5xl without a real, repeated need.
  • Build a .text-h1 class layer — compose utilities instead (Restraint).