Typography

mandatory

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 selfhosted (`assetsfontsinterlatin-400.woff2`) 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-family chain

  • Arabic / RTL: falls back to system Arabic; layout flips

    via dir="rtl" per i18n/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 touchtarget text size: 12 px with lineheight ≥ 1.4
  • Line length: target 45-75 characters per line for body text
  • Contrast: per color-roles.kmd R4 (AAA on text/bg)
  • prefers-reduced-motion does NOT affect type
  • Bold/italic combine with role, not replace: body-medium-bold

    is a STATE on body-medium, not a separate role

R8 — Code typography

Inline <code>:

  • Font: JetBrains Mono
  • Size: matches surrounding body role
  • Background: surface-variant token
  • Padding: 2px 6px
  • Borderradius: `radiussm` (4-6px depending on preset)

Block <pre><code>:

  • Font: JetBrains Mono
  • Size: 14 px (smaller than body to fit more lines)
  • Background: bg-inset token
  • Padding: 16-20 px depending on preset
  • Borderradius: `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 baseline

Anti-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 letterspacing reduction (0.10) for tighter glow
  • themes/color-schemes.kmd — syntax + body color tokens
  • themes/color-roles.kmd — text color roles
  • themes/ui-style.kmd — per-preset font stack
  • i18n/contract.kmd — locale-aware font loading
  • foundations/elements.kmd — content family uses type tokens

Source: ../home/koder/dev/koder/meta/docs/stack/specs/themes/typography.kmd