Search for a command to run...
Three typefaces — Inter for body and UI, Pythia for display, Geist Mono for code and metadata. One scale, two weights, four colors.
Typography in @delphi/ui is intentionally narrow. Three families, a six-step
size scale, two heading weights, and the same color tokens that drive
everything else. The point is consistency across surfaces — a section heading
in the design site, a card title in the app, and a dialog title in a feature
all land on the same step.
Sans · Inter
Aa
Body, UI, controls
Heading · Pythia
Aa
Page titles, display
Mono · Geist
Aa
Code, kbd, metadata
export default function Preview() {
return (
<div className="grid w-full max-w-2xl grid-cols-3 gap-6">
<div className="space-y-1">
<p className="text-fg-low font-mono text-xs tracking-widest uppercase">Sans · Inter</p>
<p className="text-fg-high font-sans text-3xl">Aa</p>
<p className="text-fg-mid text-xs">Body, UI, controls</p>
</div>
<div className="space-y-1">
<p className="text-fg-low font-mono text-xs tracking-widest uppercase">Heading · Pythia</p>
<p className="text-fg-high font-heading text-3xl tracking-tight">Aa</p>
<p className="text-fg-mid text-xs">Page titles, display</p>
</div>
<div className="space-y-1">
<p className="text-fg-low font-mono text-xs tracking-widest uppercase">Mono · Geist</p>
<p className="text-fg-high font-mono text-3xl">Aa</p>
<p className="text-fg-mid text-xs">Code, kbd, metadata</p>
</div>
</div>
);
}| Family | Token | Weights | Use |
|---|---|---|---|
| Inter | font-sans | variable, 100–900 + italic | Body, UI controls, labels — everything by default. |
| Pythia | font-heading | SemiBold (600) only | Page titles and display only. Single-weight by choice. |
| Geist Mono | font-mono | variable | Inline code, kbd, timestamps, IDs, keyboard hints. |
font-sans is the body default — applied once on <body> and inherited
everywhere. Reach for font-heading or font-mono only when the surface
deserves it. Pythia is restricted to a single weight on purpose: it's a
display face, not a working face, and the constraint keeps display hierarchy
expressed through size and color instead of weight.
export default function Preview() {
return (
<div className="w-full max-w-2xl space-y-5">
<div className="flex items-baseline gap-6">
<span className="text-fg-low w-20 shrink-0 font-mono text-xs">4xl · 36px</span>
<span className="text-fg-high font-heading text-4xl tracking-tight">Page title</span>
</div>
<div className="flex items-baseline gap-6">
<span className="text-fg-low w-20 shrink-0 font-mono text-xs">2xl · 24px</span>
<span className="text-fg-high text-2xl font-medium tracking-tight">Section heading</span>
</div>
<div className="flex items-baseline gap-6">
<span className="text-fg-low w-20 shrink-0 font-mono text-xs">lg · 18px</span>
<span className="text-fg-high text-lg font-semibold">Subsection</span>
</div>
<div className="flex items-baseline gap-6">
<span className="text-fg-low w-20 shrink-0 font-mono text-xs">base · 16px</span>
<span className="text-fg-mid text-base">Body paragraph — default prose size.</span>
</div>
<div className="flex items-baseline gap-6">
<span className="text-fg-low w-20 shrink-0 font-mono text-xs">sm · 14px</span>
<span className="text-fg-mid text-sm">Compact UI — buttons, labels, dense tables.</span>
</div>
<div className="flex items-baseline gap-6">
<span className="text-fg-low w-20 shrink-0 font-mono text-xs">xs · 12px</span>
<span className="text-fg-low text-xs">Captions, timestamps, metadata.</span>
</div>
</div>
);
}Six steps cover every surface in the system. Anything larger than text-4xl
or smaller than text-xs is out of scope — if you need it, design the
hierarchy differently.
| Step | Px | Where it shows up |
|---|---|---|
text-4xl | 36 | Page H1 (apps/design, every /system/* page). |
text-2xl | 24 | Section heading (page H2, MDX h2). |
text-lg | 18 | Subsection (MDX h3), lede paragraph, empty-state title, alert title. |
text-base | 16 | Body paragraph, card / dialog / sheet / popover titles. |
text-sm | 14 | The UI workhorse — buttons, labels, descriptions, table content. |
text-xs | 12 | Captions, timestamps, keyboard hints, filter badges, doc-table metadata. |
No text-base is the implicit prose size — MDX <p> carries no size class
and inherits it. That's deliberate; the visible class roster gets smaller.
| Weight | Token | Where |
|---|---|---|
font-normal | 400 | Body prose, table cells, descriptions, error messages. |
font-medium | 500 | Buttons, chips, labels, page H2, card / dialog titles. |
font-semibold | 600 | MDX H3 subsections, Pythia headings (display weight). |
Three weights, in that order of frequency. No font-bold, no font-light.
If text needs more emphasis than font-semibold, change the color or size,
not the weight.
The same text-fg-* tokens from Colors. Recurring pairings
the system already follows:
| Pairing | Use |
|---|---|
text-fg-high + text-4xl font-heading | Page H1 |
text-fg-high + text-2xl font-medium | Section heading (H2) |
text-fg-high + text-lg font-semibold | Subsection (H3) and alert / empty titles |
text-fg-high + text-base font-medium | Card, dialog, sheet, popover titles |
text-fg-high + text-sm font-medium | Button text, active labels, interactive titles |
text-fg-mid + text-base | Body paragraphs |
text-fg-mid + text-sm | Descriptions, secondary labels, table cells |
text-fg-low + text-xs | Captions, timestamps, hints, table headers |
text-fg-error + text-sm | Inline form errors |
text-fg-inverted + text-sm font-medium | Text on control.solid fills |
| Utility | Where |
|---|---|
tracking-tight | All display + section headings (H1 + H2) and empty-state titles. |
tracking-widest | text-xs font-mono keyboard hints in menus — spaces out abbreviations. |
tabular-nums | Kbd glyphs, OTP cells, chart values, timestamps — anything with digits. |
leading-none | Compact dense UI: labels, kbd, calendar day numbers. |
leading-snug | Form groups and list items — a touch more breathing room than none. |
Nothing else. No tracking-tighter, no leading-tight / leading-relaxed /
leading-loose in app code. Prose leans on Tailwind's default leading.
The MDX prose layer (apps/design/components/mdx-components.tsx) is the
authoritative spec for a doc page. Every surface that renders long-form text
should match this rhythm.
Body paragraphs use text-fg-mid and own their bottom margin only — headings control the gap to their own body.
Subsections sit tighter against their body than sections do — the subsection groups with what follows it.
export default function Preview() {
return (
<article className="w-full max-w-xl">
<h2 className="text-fg-high mb-4 text-2xl font-medium tracking-tight">Section heading</h2>
<p className="text-fg-mid mb-4">
Body paragraphs use{" "}
<code className="bg-surface-secondary text-tangerine-10 rounded-md px-1.5 py-0.5 font-mono text-[0.875em]">
text-fg-mid
</code>{" "}
and own their bottom margin only — headings control the gap to their own body.
</p>
<h3 className="text-fg-high mt-10 mb-2 text-lg font-semibold">Subsection</h3>
<p className="text-fg-mid mb-4">
Subsections sit tighter against their body than sections do — the subsection groups with
what follows it.
</p>
</article>
);
}| Element | Classes |
|---|---|
h2 | text-fg-high text-2xl font-medium tracking-tight mt-14 mb-4 scroll-mt-24 |
h3 | text-fg-high text-lg font-semibold mt-10 mb-2 scroll-mt-24 |
p | text-fg-mid mb-4 |
a | text-fg-high underline underline-offset-4 |
ul | mb-4 list-disc space-y-2 pl-6 |
code inline | bg-surface-secondary text-tangerine-10 font-mono text-[0.875em] rounded-md px-2 py-1 |
table | text-sm wrapper, text-fg-low thead, text-fg-mid cells, font-normal th |
Headings own the gap to their own body. Paragraphs, lists, and tables carry
mb-4 only — no top margin — so a heading's mb-* is the gap to what
follows it. The values express hierarchy directly:
h2 → body: mb-4 (16px) — a section heading sits a comfortable
distance from its first paragraph.h3 → body: mb-2 (8px) — a subsection heading sits tighter
against its body. The subsection groups with what follows it.mt-10 (h3) / mt-14 (h2) collapses with the
previous element's mb-4, so a new section breathes in proportion to
its weight.font-mono is reserved for content that's literally code or machine-flavored:
identifiers, keyboard glyphs, timestamps, paths. Never for emphasis.
| Surface | Classes |
|---|---|
Inline <code> | font-mono text-[0.875em] text-tangerine-10 bg-surface-secondary px-2 py-1 rounded-md |
<kbd> | font-mono text-sm font-medium tabular-nums leading-none text-fg-mid |
| Menu shortcut hint | font-mono text-xs tracking-widest text-fg-low |
The text-[0.875em] on inline code is em-relative on purpose — it scales
down with whatever prose size it sits inside, so a code span in a heading
reads correctly without a separate class.
Default to font-sans. Reach for font-heading only on page H1s and font-mono only on
literal code or machine values.
Pair size with color from the recurring pairings table. text-fg-mid body, text-fg-high
titles, text-fg-low captions — same intent across the system.
Use tracking-tight on display + section headings. The optical default is too loose at large
sizes.
Use font-bold to add emphasis. Change color or size instead — the system stops at
font-semibold.
Introduce sizes outside the six steps. If you need a midpoint (text-[15px], text-xl,
text-3xl), the hierarchy is wrong, not the scale.
Use font-mono for emphasis or to make text feel "technical." It's for code and machine values
only; everywhere else it reads as noise.
Use Pythia for anything but the page H1. It's a single-weight display face — body and section headings stay in Inter.