Azalea
Azalea

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:

evt_1f725978f9164fdd8674f3e8d345707d2222222222222222222222222222222222222222hana.leave.approved · root.deployment.successclaude-sonnet-5 · gpt-4o-mini · deepseek-chat

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 l and 1 a guessing game.
  • Weight and colour carry hierarchy, not size. Jumping a size to signal importance makes a dense list look ransom-noted.
  • tabular-nums on 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-tight on headings only. At 14px and below it hurts legibility.