Color roles

mandatory

Semantic color role taxonomy — the mapping from concrete tokens (bg, surface, accent, error, etc.) to the UI elements that use each. Material parity (`/styles/color/roles`). Companion to `themes/color-schemes.kmd` (which defines the actual colors) and `foundations/elements.kmd` (which says which family uses which role).

Facet Visual do Koder Design. Material parity: https://m3.material.io/styles/color/roles.

A role is a semantic slot ("primary action accent", "page background"). A token is the concrete value bound to that role in the current theme. Roles stay constant across themes; tokens vary.

R1 — The 18 canonical roles

Organized by the 5 element families from foundations/elements.kmd:

Surface roles

Role Token Use
bg --kdr-bg Page background
surface --kdr-surface Cards, sheets, raised containers
surface-variant --kdr-surface-2 Recessed/inset surfaces (search box, code block)
surface-inverse inverse-bg Snackbars, tooltips (dark on light pages, light on dark)

Content roles

Role Token Use
text --kdr-text Primary text on bg / surface
text-muted --kdr-text-muted Secondary text, captions, helper text
text-subtle --kdr-text-subtle Disabled text, placeholders
text-on-accent --kdr-accent-on Text on accent-filled surfaces

Control roles

Role Token Use
accent --kdr-accent Primary action button, active link
accent-strong --kdr-accent-strong Hover/pressed state of accent
accent-on --kdr-accent-on Foreground on accent (text/icon)
accent-tint accent at 12% opacity Selected row background, focus halo

Status roles

Role Token Use
error --kdr-error Error state border/text
error-bg --kdr-error-bg Error banner background (subtle tint)
warning --kdr-warning Warning state border/text
success --kdr-success Confirmation badge, valid input
info --kdr-info Informational banner

Decoration roles

Role Token Use
border --kdr-border Container borders, dividers
border-strong --kdr-border-strong Emphasis borders (focused container)
focus --kdr-focus Keyboard focus ring color
selection --kdr-selection Text selection bg / row selection bg

R2 — Role binding contract

Every koder_kit widget MUST bind colors via roles, not raw hex:

// ❌
Container(color: Color(0xFF3B5BFD)) // hardcoded

// ✅
Container(color: KoderTheme.of(context).accent)

The theme exposes a typed KoderColorRoles struct (planned) with one getter per role. Widget code never references tokens by string.

R3 — Inversion in dark mode

Roles flip meaning between light and dark themes via the token system, not via per-widget logic. Example:

Role Light theme step Dark theme step
bg white / slate-50 slate-900
text slate-900 slate-50
accent blue-600 blue-400 (lighter for dark bg legibility)

Steps reference themes/palette.kmd (primitive tier) — never raw hex. Widget code stays the same; the theme rebinds the tokens. The full default binding is R7.

R4 — Contrast requirements

Per color-schemes.kmd § AAA contrast, role bindings must satisfy:

Pair Min ratio
text/bg, text/surface 7.0 (AAA Normal)
text-muted/bg 4.5 (AA)
accent-on/accent 4.5 (AA)
error/error-bg 4.5
focus/bg 3.0 (UI element contrast)

high_contrast preset tightens all to ≥ 7.0.

R5 — Forbidden bindings

  • ❌ Body text on accent background (use accent-on instead)
  • text-muted for primary action labels (use text or accent-on)
  • ❌ Body text in error color (red text outside error context is alarming)
  • ❌ Custom hex on user-facing surface without going through a role

R6 — When to add a new role

Adding a role is an architectural decision (impacts every theme). Threshold:

  • Needed by ≥ 2 widgets
  • Not expressible via existing role + opacity modifier
  • Has a stable semantic meaning across themes

Process: propose via PR with the binding added to all 12 color scheme presets simultaneously (per color-schemes.kmd Catalog).

R7 — Default primitive binding (Verge base theme)

Ratified 2026-06-03 (koder-design#039). Each role resolves to a named step from themes/palette.kmd (the primitive tier) — this is what "never raw hex" (R2/R5) means concretely. Presets MAY override (a preset is just a different binding); this is the canonical default for the Verge base theme. Steps map 1:1 to meta/brand/koder-design/palette/primitives/primitives.json, so the binding is machine-checkable (contrast lives there too — R4).

Role Light Dark
bg white slate-900
surface white slate-800
surface-variant slate-100 slate-700
surface-inverse slate-900 slate-50
text slate-900 slate-50
text-muted slate-600 slate-300
text-subtle slate-400 slate-500
text-on-accent white slate-900
accent blue-600 blue-400
accent-strong blue-700 blue-300
accent-on white slate-900
accent-tint blue-50 blue-900
error red-600 red-400
error-bg red-50 red-900
warning amber-700 amber-400
success green-600 green-400
info blue-600 blue-400
border slate-200 slate-700
border-strong slate-300 slate-600
focus blue-500 blue-400
selection blue-100 blue-900

Notes:

  • accent resolves to blue-600 (not the brand fill blue-500/Verge

    #3584E4) because the role is used for text/links on light, which must clear AA (≥4.5 on white); blue-600 does, blue-500 does not. A pure accent fill with accent-on text may use blue-500.

  • The Verge v1 OLED-aware dark variant (verge.kmd §R4.3) overrides

    bg/surface to its near-black ramp (#0A0A0B…) instead of slate-900; the rest of the dark column holds. slate-900 is the palette-default floor for non-OLED dark presets.

  • high_contrast preset (R4) pushes every pair to ≥7.0 by shifting to

    the extreme steps (slate-900white, blue-700blue-300).

  • themes/palette.kmd — primitive steps every role binds to (R7)
  • color-schemes.kmd — concrete tokens per preset
  • light-dark.kmd — light/dark switching behavior
  • foundations/elements.kmd — which family uses which roles
  • interaction/states.kmd — overlay/state colors derive from roles