Foundations
Typography
Three families with one voice: Inter for the interface, Inter Tight for hierarchy, JetBrains Mono for code and numbers. Every role has a fixed size, line height and tracking.
Families
All three are open source (SIL OFL 1.1) and self-hosted in the app bundle — no third-party font CDN at runtime.
font-sansSIL OFL 1.1Interface text, controls, body copy. Optimised for small sizes on screens.
font-displaySIL OFL 1.1Headings and headline numbers. Same DNA as the UI face, tighter at large sizes.
font-monoSIL OFL 1.1Code, identifiers, log output. Also used for dense tabular numerals.
Scale
Ten roles. If a size is missing, the answer is usually to use an existing role rather than invent a value.
Numeric text
Tabular figures are mandatory wherever a number can change. Digits occupy equal width, so values do not jitter when they update.
Tabular vs proportional
Same string, two figures. Only the tabular version stays stable when the value changes.
Proportional — never use for data
Tabular — luc-numeric
NumericText component
<NumericText value={109241} kind="currency" />Using text components
| Component | API | Notes |
|---|---|---|
Heading | size="display" | "display-sm" | "heading" | "heading-sm" | "title" | Uses the display face and balanced wrapping. |
Text | variant="body-lg" | "body" | "label" | "caption" | "micro" | Plus tone: primary…disabled, accent, positive, negative. |
Code | block | Inline by default; `block` renders a monospaced panel. |
NumericText | value, kind="number" | "decimal" | "percent" | "currency" | "compact" | Always tabular; `—` for missing values. |
Kbd | children | Keyboard hints inside tooltips and docs. |
Rules
Do
- Use one
displayheading per view. - Reach for
microonly on short uppercase labels. - Set numbers in
luc-numeric, including table cells.
Don't
- Introduce a fourth family or a one-off weight.
- Use tracking overrides to squeeze text into a box.
- Set long prose in the display face.