Typography
Typography system for Koder UIs — font stacks, type scale, weights, leading/tracking tokens, and the 12 type roles every widget binds against. Material parity (`/styles/typography/fonts` + scale). Implementation: Inter (Latin) + JetBrains Mono (code) self-hosted per `#015`; per-preset overrides from `ui-style.kmd`.
Spec — Typography
Facet *isual*do Koder Design. Material parity: https://m3.material.io/styles/typography/fonts.
Two font families
| Role | Family | Source | Weights shipped |
|---|---|---|---|
| *ans (body, UI)* | Inter | self |
400 (regular), 500 (medium), 600 (semibold), 700 (bold) |
| *ono (code, monospaced)* | JetBrains Mono | self-hosted | 400, 500, 700 |
Both ship with font-display: swap per #015 so the system fallback shows immediately; webfont swaps in without FOIT.
System fallback stack (browser/OS):
font-family: 'Inter', system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-family: 'JetBrains Mono', ui-monospace, "SF Mono", Menlo, Consolas, monospace;Perpreset overrides via `uistyle.kmd (e.g., terminal_classic
uses JetBrains Mono for everything; gnome` uses Cantarell first).
R1 — Type scale (15 roles)
| Role | Size (px) | Line height | Weight | Tracking |
|---|---|---|---|---|
display-large |
57 | 64 | 400 | -0.25 |
display-medium |
45 | 52 | 400 | 0 |
display-small |
36 | 44 | 400 | 0 |
headline-large |
32 | 40 | 600 | 0 |
headline-medium |
28 | 36 | 600 | 0 |
headline-small |
24 | 32 | 600 | 0 |
title-large |
22 | 28 | 600 | 0 |
title-medium |
16 | 24 | 600 | 0.15 |
title-small |
14 | 20 | 600 | 0.10 |
body-large |
16 | 24 | 400 | 0.50 |
body-medium |
14 | 20 | 400 | 0.25 |
body-small |
12 | 16 | 400 | 0.40 |
label-large |
14 | 20 | 500 | 0.10 |
label-medium |
12 | 16 | 500 | 0.50 |
label-small |
11 | 16 | 500 | 0.50 |
Aligned with Material 3 type scale. Pixel sizes are dp (density- independent); browsers map 1:1 to CSS px below high-DPI.
R2 — Role binding contract
Every text node binds to a role, not a raw size:
// ❌
Text("Welcome", style: TextStyle(fontSize: 28, fontWeight: FontWeight.w600))
// ✅
Text("Welcome", style: KoderType.of(context).headlineMedium)The theme exposes KoderType (planned in koder_kit) with 15 getters matching the role names above. Widget code never references px sizes.
R3 — Role usage guide
| Role | Where |
|---|---|
display-* |
Hero text, marketing landings, splash screens. *ever*in app UI. |
headline-* |
Page titles, section openers, modal titles |
title-large |
Card titles, dialog titles, list-group headers |
title-medium |
App bar title, button labels (extended FAB, primary CTA) |
title-small |
Settings tile title, table column header |
body-large |
Long-form reading text (article body, doc page body) |
body-medium |
Default UI body, paragraph text, dialog body, list-item text |
body-small |
Captions, footnotes, secondary text |
label-large |
Standard button label, primary tab |
label-medium |
Chip label, badge text |
label-small |
Time stamp, micro-label, status indicator |
R4 — Per-class scale step (responsive)
Per window-size-classes.kmd, type adjusts per class:
| Class | Display | Headline | Body |
|---|---|---|---|
| Compact | -1 step | -1 step | base |
| Medium | base | base | base |
| Expanded | +1 step | base | base |
| Large | +1 step | +1 step | base |
"step" = next role up/down in the scale (e.g., headline-large -1 becomes headline-medium).
R5 — Color binding
Type color comes from color-roles.kmd:
| Default usage | Role |
|---|---|
| Body text | text |
| Secondary text | text-muted |
| Disabled text | text-subtle |
| Link | accent |
| Visited link | accent darkened by tone-shift (no separate role) |
| Code (inline) | text + surface-variant background |
| Code (block) | text on bg-inset |
Never use error red for non-error body text.
R6 — Multilingual / i18n considerations
Inter ships only Latin subset. For other scripts:
- pt-BR: covered by Latin
- East Asian (CJK): falls back to system "Noto Sans CJK" via
font-familychain - Arabic / RTL: falls back to system Arabic; layout flips
via
dir="rtl"peri18n/contract.kmd - Devanagari, etc.: system fallback
When shipping a non-Latin locale officially, vendor the corresponding Noto Sans subset (~50 KB each) alongside Inter.
R7 — Accessibility
- Minimum body text size: 14 px (body-medium)
- Minimum touch
target text size: 12 px with lineheight ≥ 1.4 - Line length: target 45-75 characters per line for body text
- Contrast: per
color-roles.kmdR4 (AAA on text/bg) prefers-reduced-motiondoes NOT affect type- Bold/italic combine with role, not replace:
body-medium-boldis a STATE on
body-medium, not a separate role
R8 — Code typography
Inline <code>:
- Font: JetBrains Mono
- Size: matches surrounding body role
- Background:
surface-varianttoken - Padding: 2px 6px
- Border
radius: `radiussm` (4-6px depending on preset)
Block <pre><code>:
- Font: JetBrains Mono
- Size: 14 px (smaller than body to fit more lines)
- Background:
bg-insettoken - Padding: 16-20 px depending on preset
- Border
radius: `radiusmd` - Overflow: scroll horizontal; never wrap
Syntax highlighting tokens come from themes/color-schemes.kmd Vocabulário de syntax (syntax_keyword, syntax_string, etc.).
R9 — Emphasized scale (Material 3 Expressive)
Material 3 Expressive ratifies a *arallel 15-role emphasized scale* same role names as R1, with heavier weight and subtle tracking/leading adjustments. The emphasized scale exists alongside R1 (it does NOT replace it) and is opted into pertextnode when visual hierarchy needs more punch than weightbumpwithin-role can provide.
R9.1 — Emphasized tokens
| Role | Weight Δ vs R1 | Tracking Δ | Use |
|---|---|---|---|
display-large-emphasized |
+200 (400→600) | -0.50 | Hero pages, splash, marketing landings |
display-medium-emphasized |
+200 | -0.25 | Hero secondary |
display-small-emphasized |
+200 | 0 | Hero tertiary |
headline-large-emphasized |
+100 (600→700) | 0 | Critical page titles, alert headers |
headline-medium-emphasized |
+100 | 0 | Important section opener |
headline-small-emphasized |
+100 | 0 | Card hero title |
title-large-emphasized |
+100 | 0 | Modal/dialog of high-priority |
title-medium-emphasized |
+100 | 0.10 | Primary CTA, app-bar title (Expressive variants) |
title-small-emphasized |
+100 | 0.05 | Featured tile title |
body-large-emphasized |
+100 (400→500) | 0.40 | Featured paragraph (article lede) |
body-medium-emphasized |
+100 | 0.20 | Strong inline (legal/key info) |
body-small-emphasized |
+100 | 0.30 | Featured caption |
label-large-emphasized |
+100 (500→600) | 0.10 | Primary action button (filled, Expressive) |
label-medium-emphasized |
+100 | 0.40 | Selected chip, active tab |
label-small-emphasized |
+100 | 0.40 | Active state indicator |
Weight deltas use variable-font axes when available (preferred); discrete weights when not.
R9.2 — Variable-font axes (preferred)
When the active font is variable (Inter VF, JetBrains Mono VF), emphasized prefers axes over weight family swap:
| Axis | Baseline (R1) | Emphasized (R9.1) |
|---|---|---|
wght (weight) |
per R1 | per R1 + delta |
opsz (optical size) |
role size px | role size px (unchanged) |
GRAD (grade) |
0 | +50 (subtle darkening without ascender shift) |
wdth (width) |
100 | 100 (unchanged — avoid auto-condensing) |
Variable axes preserve x-height and metrics, preventing layout shift when toggling baseline↔emphasized at runtime.
R9.3 — Decision tree: when to use emphasized
Is this the SINGLE most important text on the screen?
├── YES, and it's a hero context (landing, splash)
│ → display-*-emphasized
├── YES, and it's an app context (page title, primary CTA)
│ → headline/title/label *-emphasized
└── NO → does the section need visual lift above body?
├── YES (featured article lede, key paragraph)
│ → body-*-emphasized (use sparingly: max 1 paragraph per screen)
└── NO → use R1 baselineAnti-pattern: every other text emphasized. Emphasized loses meaning when overused. Rule of thumb: *t most 1 emphasized role per visual unit*(card, section, dialog).
R9.4 — Accessibility
Emphasized does NOT replace semantic HTML/ARIA. A body-large-
emphasized is still <p>, not <h2>. Screen readers MUST receive the same announcement as the baseline counterpart.
Emphasized text MUST still meet AAA contrast per R5/color-roles.kmd R4. Heavier weight does not loosen contrast requirements.
R9.5 — Surface bindings
| Surface | API |
|---|---|
| Flutter | KoderType.of(context).headlineMediumEmphasized |
| Compose | MaterialTheme.typography.headlineMediumEmphasized |
| SwiftUI | .font(.koderHeadlineMediumEmphasized) |
| Web | CSS class kds-headline-medium-emphasized + --kds-font-headline-medium-emphasized-* vars |
Surfaces MUST expose all 15 emphasized roles or none — partial implementation is a contract violation.
R9.6 — Per-preset variation
| Preset | Emphasized behavior |
|---|---|
material3 / material_expressive |
Defaults |
terminal_classic / brutalist |
Emphasized = baseline (no delta) — preset already maxes weight |
minimalist_mono |
Emphasized weight delta halved (+50 instead of +100/+200) |
cyberpunk_neon |
Emphasized adds slight letter |
Cross-link
themes/color-schemes.kmd— syntax + body color tokensthemes/color-roles.kmd— text color rolesthemes/ui-style.kmd— per-preset font stacki18n/contract.kmd— locale-aware font loadingfoundations/elements.kmd— content family uses type tokens