Typography
Geist Sans + Geist Mono on a two-layer scale with one named strong-emphasis role.
Last updated
The type system is Geist (sans UI + prose) and Geist Mono (code, data, tabular figures).
Hierarchy comes primarily from size and color. Functional headings sit at weight 400 and labels
at 500. A rare 600-weight emphasis is available only through the named text-strong role; raw
font-semibold and font-bold utilities remain forbidden.
Geist display — hero heading
Geist — section heading at 400
Geist sans — body copy.
Geist sans — caption / muted.
Geist Mono — $1,234.56 · code · numbers
The two-layer scale
text-xs … text-3xl are token-driven: on product surfaces they resolve to the product
ladder below (base = 14px, the default body size for dense, reading-heavy enterprise UI).
The docs shell you are reading is deliberately not a product surface — Fumadocs chrome and
MDX prose stay on the classic 16px documentation ladder (the --type-doc-* tokens). The
boundary is a pair of CSS bindings (see global.css): the shell binds --type-* to the doc
ladder, and every component preview (plus every Base UI portal) re-enters the product ladder.
A consuming product app gets the product ladder everywhere, automatically — the binding
defaults to product in @vegastack/design-tokens/theme.css. Re-scope any subtree by re-pointing the
--type-* variables.
Role tokens & display tier
Named roles bake the full recipe (size, line-height, weight, tracking) so call sites never
hand-set typography: text-h1(24) → text-h4(16/500) for headings, text-label /
text-label-sm (500) for UI labels, text-code / text-code-sm (mono) for code and compact
numbers. Above the heading scale sits the display tier — text-display-sm/md/lg/xl
(32/40/56/72) with a tokenized negative-tracking ramp (−0.04em → −0.06em) for marketing and
docs heros.
Weight rule: 400 for default text and h1–h3 · 500 for h4, labels, and routine emphasis · 600 only
through text-strong for a rare deliberate emphasis. At most two weights in one view; color does
the rest of the hierarchy work.
Tokens
| Token | Light | Dark |
|---|---|---|
--type-product-base | {"fontSize":"0.875rem","lineHeight":"calc(21 / 14)"} | {"fontSize":"0.875rem","lineHeight":"calc(21 / 14)"} |
--type-doc-base | {"fontSize":"1rem","lineHeight":"calc(1.5 / 1)"} | {"fontSize":"1rem","lineHeight":"calc(1.5 / 1)"} |
--text-label | {"fontSize":"0.875rem","lineHeight":"1.25rem","fontWeight":"500","letterSpacing":"-0.01em"} | {"fontSize":"0.875rem","lineHeight":"1.25rem","fontWeight":"500","letterSpacing":"-0.01em"} |
--text-strong | {"fontSize":"0.875rem","lineHeight":"1.25rem","fontWeight":"600","letterSpacing":"-0.01em"} | {"fontSize":"0.875rem","lineHeight":"1.25rem","fontWeight":"600","letterSpacing":"-0.01em"} |
--text-label-sm | {"fontSize":"0.75rem","lineHeight":"1rem","fontWeight":"500","letterSpacing":"-0.01em"} | {"fontSize":"0.75rem","lineHeight":"1rem","fontWeight":"500","letterSpacing":"-0.01em"} |
--text-code | {"fontSize":"0.8125rem","lineHeight":"1.25rem","fontWeight":"400","letterSpacing":"0em"} | {"fontSize":"0.8125rem","lineHeight":"1.25rem","fontWeight":"400","letterSpacing":"0em"} |
--text-code-sm | {"fontSize":"0.75rem","lineHeight":"1rem","fontWeight":"400","letterSpacing":"0em"} | {"fontSize":"0.75rem","lineHeight":"1rem","fontWeight":"400","letterSpacing":"0em"} |
--font-family-sans→ Geist (UI + prose)--font-family-mono→ Geist Mono (numbers, code, currency)--font-family-serif→ Newsreader serif accent (display emphasis + pull-quotes only, never running text)--font-family-display→ Geist display tier (split token so marketing can retune independently)--font-family-pixel→ Geist Pixel Square — THE single sanctioned pixel flourish (one deliberate hero use per surface)
Use font-sans, font-mono, font-serif utilities (bridged from the runtime tokens).
Font loading
The font-delivery contract (CX-10): Geist Sans / Geist Mono ship from the geist npm
package (geist/font/sans, geist/font/mono; the five Geist Pixel cuts live at
geist/font/pixel — this system sanctions GeistPixelSquare only). Newsreader loads via
next/font/google, which self-hosts the subset woff2 at build time — so the static export
serves every face itself: no render-blocking external request, no layout shift, and the fonts
work offline. app/global.css binds the loaded faces onto the runtime font tokens (with system
fallbacks):
:root {
--font-family-sans:
var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
--font-family-mono: var(--font-geist-mono), ui-monospace, monospace;
}This is the one-file override point: a consuming app supplies its own
faces by redefining --font-family-sans / -mono / -serif, and every
font-sans / font-mono / font-serif utility repaints — the @theme inline bridge maps these tokens to Tailwind's font utilities.