Typography
Typography in helix-ui is four token groups: family, size, weight, and line-height. Everything you set on text resolves to one of them. There is no free-typed font-size: 15px in component CSS — if a size is missing, it isn’t in the system.
Font family
Two families ship. The default (sans) leads with Inter and falls back through the OS UI fonts; mono is a system monospace stack.
| Token | Value |
|---|---|
--helix-ui-font-family-sans | Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Helvetica, Arial, sans-serif |
--helix-ui-font-family-mono | ui-monospace, SFMono-Regular, Menlo, Consolas, monospace |
Inter is expected but never required. If it hasn’t loaded, ui-sans-serif / system-ui render the same metrics closely enough that layout doesn’t shift. Use mono for code, keyboard glyphs, and anything that needs column alignment (timestamps, IDs, numeric tables).
CJK fallback
Inter carries no Hangul, Kana, or Han glyphs. A mixed-script line like 제품·AI 프롬프트 would otherwise render the Latin in Inter and the Hangul through whatever OS CJK font the browser picks — usually a size larger — so the line visibly jumps mid-word. The fix is to put an OS CJK font ahead of Inter per language, chosen so its Latin glyphs match its CJK metrics. From apps/site/src/site.css:
:lang(ko) { font-family: 'Pretendard Variable', Pretendard, 'Apple SD Gothic Neo', 'Malgun Gothic', 'Noto Sans KR', Inter, ui-sans-serif, system-ui, sans-serif; word-break: keep-all; /* Korean breaks at spaces, never inside a word */ line-break: strict; overflow-wrap: anywhere;}:lang(ja) { font-family: 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic', 'Meiryo', 'Noto Sans JP', Inter, ui-sans-serif, system-ui, sans-serif; word-break: normal; /* CJK with no spaces: let kinsoku break anywhere */ line-break: strict;}:lang(zh) { font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans SC', Inter, ui-sans-serif, system-ui, sans-serif; word-break: normal; line-break: strict;}Two rules ride along with the stack. Korean keeps word-break: keep-all because its words are space-separated — breaks land on spaces, not inside a word. Japanese and Chinese use word-break: normal so the browser’s kinsoku-shori (line-break: strict) can break between characters instead of freezing a whole sentence and overflowing. Phrase-level stickiness (읽을 수 있는, AI の) is joined with NBSP at the translation layer — CSS can’t infer a phrase boundary.
Size scale
Eight steps, xs through 4xl, anchored on a 16px body. The full range spans 3× — 12px caption to 36px display.
| Token | Size | Role |
|---|---|---|
--helix-ui-font-size-xs | 12px | Caption, metadata |
--helix-ui-font-size-sm | 14px | Compact body, dense tables |
--helix-ui-font-size-md | 16px | Default body |
--helix-ui-font-size-lg | 18px | Lead paragraph |
--helix-ui-font-size-xl | 20px | Subheading |
--helix-ui-font-size-2xl | 24px | Section heading |
--helix-ui-font-size-3xl | 30px | Page heading |
--helix-ui-font-size-4xl | 36px | Display |
On viewports ≤767px the static build shrinks the top of the scale about 10% — md stays readable at 14px, 4xl drops to 32px — via a @media (max-width: 767px) override on the same variables. Components don’t opt in; they inherit it because they read the token.
Weight
Four weights. Inter ships all of them; the fallbacks synthesize what they lack.
| Token | Value | Use for |
|---|---|---|
--helix-ui-font-weight-regular | 400 | Body copy |
--helix-ui-font-weight-medium | 500 | Slight emphasis, labels |
--helix-ui-font-weight-semibold | 600 | Buttons, headings |
--helix-ui-font-weight-bold | 700 | Strong emphasis |
Headings sit at semibold (600), not bold. Bold is loud enough that reserving it for the rare strong emphasis keeps it meaningful. There is no 300 or 800 — thin text fails contrast at small sizes, and 800 reads the same as 700 in most UI contexts.
Line-height
Three ratios, unitless so they scale with whatever size they land on.
| Token | Value | Use for |
|---|---|---|
--helix-ui-font-lineHeight-tight | 1.2 | Headings, large text |
--helix-ui-font-lineHeight-normal | 1.5 | Body — the default |
--helix-ui-font-lineHeight-loose | 1.75 | Long-form reading |
The pairing rule: as size goes up, line-height comes down. Body at 16px wants 1.5 for readability; a 30px heading at 1.5 leaves a gap you could park a car in, so headings take tight (1.2). The Text component encodes this automatically — see below.
The Text component
Text maps props to tokens so you never touch a variable directly for prose. size and weight alias the scales above; tone aliases the color roles from Color.
import { Text } from '@helix-ui/core';
<Text as="h1" size="3xl" weight="semibold">Billing</Text><Text size="md">Your plan renews on the 1st.</Text><Text size="sm" tone="muted">Last synced 2 minutes ago</Text>One behavior worth knowing: at xl and above, Text switches its own line-height to tight — so <Text size="2xl"> is 24px at 1.2, while <Text size="md"> is 16px at 1.5. You get the size/line-height pairing without setting it.
Reach for raw tokens only outside prose — a custom layout, a third-party surface:
.doc-callout { font-family: var(--helix-ui-font-family-sans); font-size: var(--helix-ui-font-size-sm); font-weight: var(--helix-ui-font-weight-medium); line-height: var(--helix-ui-font-lineHeight-normal);}Typography can move — via DNA
The DNA typography gene rebinds the family and rescales sizes at runtime. Its wildtype allele is sans (Inter, scale 1.0); others swap the whole stack — grotesk → Geist, rounded → Nunito, serif → Lora, mono → a monospace UI. A separate typeScale gene sets the ratio between steps (wildtype 1.2); major (1.25) or golden (1.618) widen the gap between body and display without you editing a single size.
Because sizes are re-derived from md × ratio^step, changing the gene rewrites --helix-ui-font-size-* on the DNA provider’s wrapper. Every Text and every component under it follows through the cascade with no re-render.
Usage rules
- Body is
md/16px atnormal/1.5. Drop tosm/14px only in dense UI (tables, side panels), never for primary reading. - Headings are
semibold, notbold. Save 700 for the one word that has to shout. - Let size drive line-height. Big text takes
tight; don’t set 1.5 on a heading.Textdoes this for you abovelg. - Never inline a px size. If the value you want isn’t a token, the scale is telling you to pick the neighbor.
- Monospace for anything that aligns. Numeric columns, IDs, and diffs read wrong in a proportional font.
The machine-readable font tokens — DTCG types, build outputs, the naming grammar — live in the Tokens reference.