The type system

TypeSentient

Typography existsto honour content.

Robert Bringhurst, The Elements of Typographic Style

One ladder, from a table cell to a hero. Every step carries its size, its leading, its tracking and its typeface, so nobody has to feel their way to a decision that was already made.

01

The face is part of the step

Three typefaces, and none of them is something you pick. A step carries its face. Mono holds micro and data, serif begins at subhead at 24px and runs to the top, and the sans holds everything between.

ui and data are the same size and different steps, because they are different jobs. One is a control; the other is an identifier that has to be read character by character. Numbers do not need it — Nunito's digits are all one width, so a column of figures already lines up in the sans.

24px is not a taste. Sentient's stroke contrast is 2.28:1 against Nunito's 1.26, and its thinnest stroke is 47 units of a thousand. That hairline reaches one device pixel on a standard-density screen at 21.2px. Below that it renders patchy and the face reads as a fault rather than a choice; above it the same contrast is the reason to use it. So the boundary sits at the first step clear of the threshold, and it holds everywhere: every heading from 24px up is serif, in the product as much as on the site.

Nothing asks for a face. A component asks for a step, and the face arrives with it. That holds in the product and on the site alike: a subsection heading in a settings pane and one on a landing page are the same step, so they are the same face.

One limit, written down rather than discovered later. Sentient is a Latin face: 389 glyphs, stopping at Latin Extended-B, with no Cyrillic and no Greek. Odin ships in Latin scripts, so nothing is affected today. If that changes, the boundary is the lever — a face can only own the steps the language it is set in allows, and every step from 24px up would have to fall back to the sans in scripts Sentient does not cover.

Aa Rg

Sentient

subhead → hero · 24–76px

Six steps, from a subsection heading to a hero. Everything at 24px and above, in the product as well as on the site.

400, 500 & 700  ·  no SemiBold
Aa Rg

Nunito

caption → lead · 12–20px

Five steps, and most of the product. Every control, label, value and paragraph a person reads or clicks.

400, 500 & 600
Aa Rg

JetBrains Mono

micro · data · 11 & 13px

Two steps, both about exactness. Uppercase micro-labels, and identifiers where 0 and O must not be confused. Not for numbers — Nunito's figures are already tabular.

400 & 500
02

The scale

Thirteen steps, from 11px to 76px. There is no app scale and no website scale. A table cell in the product and a table cell on the marketing site are the same job, and a heading is a heading wherever it appears.

The distinction that matters is interface text against reading text, and both exist everywhere. A marketing page has buttons; a settings pane has paragraphs. So the ladder holds both: ui at 13px for what you scan, body at 17px for what you read.

The pixel figures below are at 100 per cent, which is where the app opens. Every size in the product is a rem multiplied by the density setting, so type moves with padding, gaps and radii rather than on its own — two notches either way, holding its shape at every step. Spacing owns that setting.

Every step carries its own leading and tracking, and the serif steps carry different numbers from the sans ones. Sentient's x-height is 3.7 per cent larger than Nunito's, so it fills more of the line and its leading is raised to match. It is also fitted far more tightly — its sidebearings are 0.62 of Nunito's at equal apparent size — so it needs proportionally less negative tracking to sit right. Both numbers come out of the font files rather than off an eye.

--text-micro
11px @100%  ·  0.6875rem
1.45 leading  ·  +0.16em tracking
JetBrains Mono
Uppercase labels, table headers, annotations
SEGMENT · LAST 30 DAYS
--text-caption
12px @100%  ·  0.75rem
1.45 leading  ·  0 tracking
Nunito
Timestamps, badges, helper text
Saved 4 minutes ago by Priya
--text-ui
13px @100%  ·  0.8125rem
1.5 leading  ·  0 tracking
Nunito
The interface default. Inputs, menus, nav, table cells
Send at 09:00 in the recipient's timezone
--text-data
13px @100%  ·  0.8125rem
1.5 leading  ·  0 tracking
JetBrains Mono
Identifiers, codes, hashes. Where 0 and O must not be confused
ORD-4821-QK · a3f9c2 · 09:41:22
--text-body-tight
15px @100%  ·  0.9375rem
1.6 leading  ·  0 tracking
Nunito
Descriptions and help text inside the product
Everyone matching the rules below is added automatically.
--text-body
17px @100%  ·  1.062rem
1.65 leading  ·  0 tracking
Nunito
Reading text
Odin watched 1.2 million events this week and acted on 340 of them.
--text-lead
20px @100%  ·  1.25rem
1.5 leading  ·  -0.005em tracking
Nunito
Opening paragraphs, card titles
Everything that happened while you were away.
--text-subhead
24px @100%  ·  1.5rem
1.4 leading  ·  -0.006em tracking
Sentient
Subsection headings
Lapsed buyers, last 90 days
--text-heading
30px @100%  ·  1.875rem
1.3 leading  ·  -0.007em tracking
Sentient
Section headings
What changed overnight
--text-title
38px @100%  ·  2.375rem
1.19 leading  ·  -0.011em tracking
Sentient
Page titles
Everything Odin noticed
--text-display
48px @100%  ·  3rem
1.12 leading  ·  -0.012em tracking
Sentient
Display, empty states, marketing
Nothing needs your attention.
--text-display-lg
60px @100%  ·  3.75rem
1.09 leading  ·  -0.012em tracking
Sentient
Large display
Ask Odin anything
--text-hero
76px @100%  ·  4.75rem
1.06 leading  ·  -0.014em tracking
Sentient
Hero. One per page, at most
It already knew.
03

Weight is the second axis

Twelve steps and three weights, applied independently. A section header is body-tight at 600. A field label is ui at 500. A value beside it is ui at 400. Three different jobs, one step, no new names.

That is why weight is an axis rather than part of a token. Fold it in and every size needs a name per weight, and the names multiply faster than the sizes do.

Audience segment400  Regular

Values, descriptions, table cells. Anything being read rather than identified

Audience segment500  Medium

Field labels, nav items, active states, inline emphasis

Audience segment600  SemiBold

Headings and buttons, on the sans steps only

Not every pairing is legal

A free axis would let anyone write a combination the fonts cannot render. Sentient ships at 400 and 500 — there is no SemiBold, so a 600 heading at 24px would be synthesised by the browser and look it. And 600 at 11px in the mono is a smear. So each band permits a range, and the range is checkable.

StepsFaceWeightsDefaultWhy
micro · dataJetBrains Mono400 · 500400600 fills the counters at 11px, and a bolded identifier reads as emphasis it does not have.
caption → leadNunito400 · 500 · 600400The full range. This is where interface hierarchy is built.
subhead → heroSentient400 · 500 · 700400The family has no SemiBold, so 600 is unavailable. 700 is for a hero at full viewport, nowhere smaller.

A heading is not a size. It is a weight applied to the size the surrounding text already uses.

04

Labels and headings

These two carry most of the text in the product, so their weights decide how the whole thing feels.

A label identifies a control. It sits beside a value at the same size and often the same colour, so weight is what separates them. 500 does that without shouting, and it leaves 600 meaning heading everywhere in the system. A settings pane should not read as twenty headings stacked down the page.

Contact details
Email address priya@example.com
Timezone Europe/Stockholm
Used for send times and reporting.
Contact detailsbody-tight 15 · 600 · a heading, so it looks like one
Email addressui 13 · 500 · identifies the control beside it
priya@example.comui 13 · 400 · the content
Used for send timescaption 12 · 400 · subordinate to both

The heading component follows the same rule: 600 while the steps are sans, 500 at subhead and above, where Sentient has no SemiBold to give.

A field label and a section header are different jobs and take different weights, so a component that renders both needs to know which one it is rendering. That is a prop, not a size.

05

The one place it is a choice

Odin chat is the exception, and the only one. A conversation is long-form reading, and how someone wants to read for an hour is theirs to decide. Serif is the default; sans is one tap away. Everywhere else the face follows the step.

It is also the only place where two faces share a step, and that creates a problem: set at the same size they do not look the same size. Sentient's x-height is 0.502em against Nunito's 0.484em, and its ascenders and descenders are shorter, so it sits larger on the line at the same specified size.

The fix is not a second step and not a hand-tuned number. font-size-adjust: 0.484 tells the browser to render whichever face is in use at the size that gives Nunito's x-height, working it out from that font's own metrics. Sentient lands at 96.4 per cent, so the 17px step renders at 16.4 and the two faces match. Swap the serif for another one day and it still holds.

Odin chat
You

Which segments moved most last week, and what changed in them?

Odin

Three moved by more than ten per cent. The largest was Lapsed buyers, up 4,120 people, nearly all of them since Tuesday. The other two are smaller and look seasonal. The same rise happened in the same week last year.

06

What moves

Seven of today's eleven tokens stop existing, because they were weights wearing the costume of a size. Two sizes change: reading text moves off 14px, where it was smaller than some of the headings above it, and the smallest label moves to the mono face it was already behaving like.

TodayBecomesWhy
caption11 / 400--text-micro11 / 400Moves to the mono face. It was always a label, never prose.
label12 / 400--text-caption12 / 400Same size, honest name.
ui13 / 400--text-ui13 / 400Unchanged. The size was always right.
ui-emphasis13 / 500--text-ui13 / 500Not a token. A weight applied to one.
title-2xs13 / 600--text-ui13 / 600Not a token. A heading is a weight.
body14 / 400--text-body-tight15 / 400Was reading text at an interface size. Now it is reading text.
body-emphasis14 / 500--text-body-tight15 / 500Not a token.
title-xs14 / 600--text-body-tight15 / 600Not a token.
title-sm16 / 600--text-body17 / 600Not a token.
title20 / 600--text-lead20 / 600Not a token.
title-lg24 / 600--text-subhead24 / 600Not a token.

Nothing here is visible to anyone using the product. Someone building a screen asks for a table cell or a page title, and the size, the leading, the tracking and the face arrive together. The only person who ever chooses a typeface is the one reading a conversation.

Printing should be invisible.

Beatrice Warde, The Crystal Goblet, 1930

Warde was arguing about wine glasses. A crystal goblet is better than a gold one because you can see through it to the wine, and type is the same: the better it is set, the less of it you notice and the more of what is behind it you do. Ninety-five years on, in a product read for hours at thirteen pixels, the argument has not moved.