Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

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

Geist — the quick brown fox--text-3xl · 24 / 32px
Geist — the quick brown fox--text-2xl · 20 / 28px
Geist — the quick brown fox--text-xl · 18 / 26px
Geist — the quick brown fox--text-lg · 16 / 24px
Geist — the quick brown fox--text-base · 14 / 21 · default bodypx
Geist — the quick brown fox--text-sm · 12 / 16px
Geist — the quick brown fox--text-xs · 11 / 16px

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 tiertext-display-sm/md/lg/xl (32/40/56/72) with a tokenized negative-tracking ramp (−0.04em → −0.06em) for marketing and docs heros.

Geist — the quick brown fox--text-display-xl · 4.5rem / 4.75rem · −0.06em
Geist — the quick brown fox--text-display-lg · 3.5rem / 3.75rem · −0.05em
Geist — the quick brown fox--text-display-md · 2.5rem / 2.75rem · −0.045em
Geist — the quick brown fox--text-display-sm · 2rem / 2.25rem · −0.04em
Geist — the quick brown fox--text-h1 · 1.5rem / 2rem · −0.02em
Geist — the quick brown fox--text-h2 · 1.25rem / 1.75rem · −0.015em
Geist — the quick brown fox--text-h3 · 1.125rem / 1.5rem · −0.01em
Geist — the quick brown fox--text-h4 · 1rem / 1.375rem
Geist — the quick brown fox--text-label · 0.875rem / 1.25rem
Geist — the quick brown fox--text-label-sm · 0.75rem / 1rem
Geist — the quick brown fox--text-code · 0.8125rem / 1.25rem
Geist — the quick brown fox--text-code-sm · 0.75rem / 1rem

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

TokenLightDark
--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-serifNewsreader 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-pixelGeist 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.

Do
Apply the type tokens — text-base body, text-h*/text-label* roles, font-mono for numbers.
Don't
Hand-set font-size/line-height/weight, or reach past text-3xl (use the display tier).

On this page