Typography
Two faces, seven steps. Small on purpose: these are dense operational tools, and the reader is scanning a list, not reading prose.
The decision
Plus Jakarta Sans for the interface, Geist Mono for identifiers. Both self-hosted, both exposed as tokens.
Interface
Plus Jakarta Sans
A geometric humanist with real character — single-storey a, open g, tapered terminals — that still holds up at 13px. --font-sans
Identifiers
Geist Mono
Drawn for developer tooling. Slashed zero by default, 1/l/I separated without effort. --font-mono
Why the mono choice mattered more than the sans
Averoa renders commit SHAs, correlation ids, event types and model names in monospace — the content people compare character by character, across four services. Until this decision there was no mono face at all: Tailwind's default resolved to SF Mono on a Mac, Consolas on Windows and DejaVu on Linux.
The one place glyph shapes carry meaning was the one place they differed on every machine. These are the strings the platform actually shows:
The pairs that have to stay apart
0O 1lI 5S 8B rn m
Set in the interface face, for contrast
0O 1lI 5S 8B rn m
Zero is slashed and ligatures are off, both set in tokens.css. Ligatures turn != into ≠ and -> into → — a nicety when you are writing code and a lie when you are displaying a log line.
Self-hosted — and why the earlier objection was wrong
This system originally shipped the system stack and argued against a web font on the grounds that it costs a network round trip on first paint. That objection was right about the cost and wrong about the mechanism.
next/font downloads the face at build time and serves it from our own origin, with a preload and a size-adjusted fallback — no request to Google at runtime, no third-party connection, and no layout shift when it swaps in. The cost that justified the system stack is not there.
Both faces are variable, self-hosted, and preloaded with a size-adjusted fallback, so the swap costs nothing at runtime and shifts no layout.
/* Azalea exposes both faces as tokens, defaulting to the
system stacks — so a consumer that wants different ones
changes two variables and nothing else. */
--font-sans: ui-sans-serif, system-ui, …
--font-mono: ui-monospace, "SFMono-Regular", "Geist Mono", …
/* app/layout.tsx */
const sans = Plus_Jakarta_Sans({ subsets: ["latin"], variable: "--font-sans" });
const mono = Geist_Mono({ subsets: ["latin"], variable: "--font-mono" });
<html className={`${sans.variable} ${mono.variable}`}>A consumer that does nothing gets the system stacks and a service that looks deliberate. That is the point of shipping them as tokens rather than baking them into the stylesheet.
Why this pair, and what it cost
This system shipped Inter first, on the argument that a neutral face serves dense data better than a characterful one. That argument is still true — it just is not the only thing that matters.
Inter is everywhere, and a design system whose whole job is to carry a brand should probably not sound like everyone else. Plus Jakarta Sans has a voice — the single-storey a, the open g, the slight warmth — without being a display face pretending to be a UI face. Its x-height is tall and its apertures are open, which is what keeps it readable at the sizes these tools use.
It was also drawn for Jakarta's city identity. For PT Averoa Teknologi Indonesia that is a reason rather than a coincidence, and brand meaning is a fair tiebreak between two faces that both do the job.
The honest cost: Plus Jakarta is wider than Inter. A dense table fits slightly less per line. That is the trade — a little horizontal room for a voice — and it is worth re-checking on the widest table you have before assuming it is free.
Geist Mono pairs with it on shared geometric construction, so the two do not fight — but it runs cooler and tighter, so a hash beside a sentence reads as a different kind of thing. That contrast is the point of having two faces rather than one.
The scale
Page title
24px / 600 / -0.025em
Deployment succeeded on staging
One per page.
Section title
18px / 600
Deployment succeeded on staging
A card or a page section.
Subsection
14px / 600
Deployment succeeded on staging
CardTitle, group headings.
Body
14px / 400
Deployment succeeded on staging
The default. Most interface text.
Secondary
12px / 400 / muted
Deployment succeeded on staging
Timestamps, hints, counts.
Label
11px / uppercase
Deployment succeeded on staging
Badges, column headers.
Mono
12px / mono
Deployment succeeded on staging
Ids, commits, code, model names.
Rules
- Mono for anything a person will compare character by character — commit hashes, ids, model names. A proportional font makes
land1a guessing game. - Weight and colour carry hierarchy, not size. Jumping a size to signal importance makes a dense list look ransom-noted.
tabular-numson any column of figures. Proportional digits make a column of costs impossible to compare down, and it is the comparison that people are there for.tracking-tighton headings only. At 14px and below it hurts legibility.