Overrides API — named subpart contract for KDS components

draft

Defines the named-subpart Overrides API every KDS component MUST expose so consumers can swap subparts (Root, Title, Body, Icon, …) without forking the component. Inspired by Base Web's Overrides API. Three override modes per subpart: `style`, `component`, `props`. Owner sign-off required before ratification; this spec LAYS OUT the contract for review.

Spec — Overrides API (draft, pending sign-off)

*tatus* v0.1.0 Draft (20260522). Owner sign-off required. *o implementation until ratified.*Backlog: tools/design-gen#095.

R1 — Why

KDS components today are monolithic. A consumer wanting to swap the close-button icon in Banner, or replace the title element with a custom node, must either:

  1. Fork the entire component (breaks SDK upgrade path).
  2. Hack via portals or DOM manipulation (breaks accessibility,

    styling, and SSR).

  3. Wait for a custom prop to be added by the KDS team (slow + per-

    request, doesn't scale).

The Overrides API resolves this by making *very KDS component a customization surface*— consumers swap named subparts without forking.

R2 — Vocabulary

Term Meaning
*ubpart* A named, addressable region of a component. Every component has at least Root; concrete components add names like Title, Body, Icon, CloseButton, Item.
*verride* The consumer-supplied value for a subpart.
*verride mode* One of style (CSS-only delta), component (full swap), props (extra props merged into the default render).
*verride map* The single overrides prop on a KDS component — a Map<SubpartName, Override> containing zero or more entries.

R3 — Subpart contract

Every KDS component spec MUST declare a R-subparts: section enumerating the component's subparts. Format per subpart:

### Subpart `<Name>`

- Role: <one sentence>
- Default element: <tag or component>
- Default props: <list>
- Default style: <reference to design tokens>
- Overridable via: style | component | props (any subset)

The set is *losed*— only subparts listed in the spec are overridable. Adding a subpart is a specamendment change with signoff.

Every component has the implicit Root subpart by default; specs SHOULD list it explicitly for documentation symmetry.

R4 — Override modes

Mode style

overrides: {
  Title: { style: { color: 'var(--kdr-danger)', fontWeight: 600 } }
}

Merges the supplied style object onto the subpart's default style. Conflicting keys take the override. Implementation MUST use the platform's canonical style cascade (CSS for web, TextStyle merge for Flutter). Style overrides MUST NOT be allowed to break R8 a11y contracts (contrast, focus-ring, hit target) — components MAY emit a runtime warning + audit failure.

Mode component

overrides: {
  Icon: { component: MyCustomIcon }
}

Replaces the subpart's element entirely. The replacement receives the same props the default would have, plus any consumer-supplied overrides.<name>.props (merged). Replacements MUST honor the subpart's semantic role (e.g. Icon must still be an icon-shaped element so screen readers find it).

Mode props

overrides: {
  CloseButton: { props: { 'aria-label': 'Dismiss notification' } }
}

Merges extra props onto the default subpart render. Conflicting keys take the override. SHOULD be used for a11y / data-attribute additions; SHOULD NOT be used for full structural replacement (use component).

Combined

overrides: {
  Title: { style: { color: 'red' }, props: { 'data-tone': 'danger' } }
}

Any combination is allowed. Resolution order: props merged first, then style merged onto the resulting style prop, then component swap if present.

R5 — Naming conventions

Subparts are *ascalCase*singular nouns (Root, Title, Icon, CloseButton, Item). Where a component renders a list, the per- item subpart is named in the singular and applies to every item; position-specific overrides are addressed via the item's data attribute (overrides.Item + a props.data-index check in the consumer's component override).

Avoid:

  • Hyphens (close-button ❌ — use CloseButton).
  • Generic names (Element, Inner ❌ — name the role).
  • Implementation-specific names (Anchor ❌ when the real role is

    Link).

R6 — Per-platform implementation

Surface Default mechanism
Flutter (koder_kit) overrides: Map<String, KoderOverride> prop on every component. KoderOverride is a {style, component, props} record.
Web (templ in KDS docs) Templ helper @withOverrides(overrides, "Title", defaultProps, defaultStyle) resolves the override at render time.
Web SDK (koderwebkit, JS/TS) overrides prop matching Base Web's signature; same three modes.
Android native Compose OverridesMap parameter on every Koder* composable.
iOS native Swift Overrides struct matching the three-mode contract.

Cross-platform consumers SHOULD declare overrides in a shared .toml or .json resource and load per-platform — this is a koder_kit followup (RFC006 distribution channels).

R7 — Audit

koder-spec-audit overrides (new subcommand, follow-up) walks every component spec under meta/docs/stack/specs/components/, asserts:

  1. Each component spec has a R-subparts: section.
  2. Every subpart listed has the four required fields (Role / Default

    element / Default props / Default style / Overridable via).

  3. Subpart names are PascalCase singular.
  4. No two components in the same family use conflicting subpart names

    for the same role.

Failure exits 1; release engineering blocks merge.

R8 — A11y guard

Overrides MUST NOT break:

  • *ontrast* any style override that drops the rendered text below

    WCAG AA (4.5:1) fails the visual-regression audit (#086).

  • *ocus ring* focus-ring color override below 3:1 contrast against

    the surface fails.

  • *it target* replacing a CloseButton with a component

    rendering below 44 × 44 dp fails the touch-target audit (specs/a11y/touch-targets.kmd).

  • *emantic role* replacing a Title with a non-heading element

    must include aria-level or fail.

These guards are runtime warnings in dev mode + audit failures in CI.

R9 — Backwards-compatibility

Existing components without an overrides prop continue to work — the prop is *ptional with default empty map* Per-component migration to publish subparts ships incrementally; the global audit warning goes from "advisory" to "error" 1 minor after a critical mass of components have ratified subparts (≥ 80 % of the gallery).

R10 — Tests (T-suite)

ID Test
T1 Component renders with no overrides — golden equals baseline.
T2 overrides.Title.style swaps the rendered color; golden differs.
T3 overrides.Icon.component swaps the icon element; new element rendered, default element absent.
T4 overrides.CloseButton.props adds the new prop; default props preserved; conflicting prop overridden.
T5 Override on an unknown subpart name MUST emit a dev-mode warning + fail audit.
T6 Combined modes resolve in the documented order (props → style → component).
T7 A11y guard fires when a contrast-breaking style is supplied.
T8 SSR renders the override identically to client (no hydration delta).

R11 — Migration plan

  1. Ship this spec (draft → ratified after owner sign-off).
  2. Open percomponent subtickets (095.A–095.Z) to add R-subparts:

    sections to every existing component spec.

  3. Implement the runtime overrides prop in koder_kit first

    (Flutter — largest consumer surface). WebSDKNative follow.

  4. Migrate Koder products that fork components today (audit reports

    "fork count by component" — koder-spec-audit overrides --forks) to the override path.

R12 — Open questions

  1. Should KDS ship a default "overrides composer" helper that lets a

    consumer apply a single override map across an entire subtree (Providerstyle), or is percomponent prop enough?

  2. Should overrides be reactive — i.e. can an override depend on

    component state (overrides.Item: ({ active }) => ({...}))? Base Web allows this; it adds complexity.

  3. Should the style mode accept design tokens by name ({ color: 'danger' })

    instead of literal values, with the resolver in koder_kit doing the lookup?

Sign-off

Role Owner
Author @rpm (20260522)
Ratification pending_
Implementation backlog tools/designgen#095 + percomponent sub-tickets

Source: ../home/koder/dev/koder/meta/docs/stack/specs/develop/overrides-api.kmd