Overrides API — named subpart contract for KDS components
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 (2026
0522). 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:
- Fork the entire component (breaks SDK upgrade path).
- Hack via portals or DOM manipulation (breaks accessibility,
styling, and SSR).
- 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❌ — useCloseButton). - Generic names (
Element,Inner❌ — name the role). - Implementation-specific names (
Anchor❌ when the real role isLink).
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:
- Each component spec has a
R-subparts:section. - Every subpart listed has the four required fields (Role / Default
element / Default props / Default style / Overridable via).
- Subpart names are PascalCase singular.
- 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
CloseButtonwith a componentrendering below 44 × 44 dp fails the touch-target audit (
specs/a11y/touch-targets.kmd). - *emantic role* replacing a
Titlewith a non-heading elementmust include
aria-levelor 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
- Ship this spec (draft → ratified after owner sign-off).
- Open per
component subtickets (095.A–095.Z) to addR-subparts:sections to every existing component spec.
- Implement the runtime
overridesprop inkoder_kitfirst(Flutter — largest consumer surface). WebSDKNative follow.
- 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
- Should KDS ship a default "overrides composer" helper that lets a
consumer apply a single override map across an entire subtree (Provider
style), or is percomponent prop enough? - Should overrides be reactive — i.e. can an override depend on
component state (
overrides.Item: ({ active }) => ({...}))? Base Web allows this; it adds complexity. - Should the
stylemode 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 (2026 |
| Ratification | pending_ |
| Implementation backlog | tools/design |