Overrides API — named subpart contract for KDS components
develop specs/develop/overrides-api.kmd
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.
Quando esta spec se aplica
Todos os triggers
- Author a new KDS component spec
- Add a configurable subpart to an existing component
- Audit a Koder product for fork-based component drift
Corpo da especificação
Spec — Overrides API (draft, pending sign-off)
Status: v0.1.0 Draft (2026-05-22). Owner sign-off required. No 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 every KDS component a customization surface — consumers swap named subparts without forking.
R2 — Vocabulary
| Term | Meaning |
|---|---|
| Subpart | A named, addressable region of a component. Every component has at least Root; concrete components add names like Title, Body, Icon, CloseButton, Item. |
| Override | The consumer-supplied value for a subpart. |
| Override mode | One of style (CSS-only delta), component (full swap), props (extra props merged into the default render). |
| Override 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 closed — only subparts listed in the spec are overridable. Adding a subpart is a spec-amendment change with sign-off.
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 PascalCase 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 (koder_web_kit, 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
follow-up (RFC-006 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:
- Contrast: any style override that drops the rendered text below WCAG AA (4.5:1) fails the visual-regression audit (#086).
- Focus ring: focus-ring color override below 3:1 contrast against the surface fails.
- Hit target: replacing a
CloseButtonwith a component rendering below 44 × 44 dp fails the touch-target audit (specs/a11y/touch-targets.kmd). - Semantic role: replacing a
Titlewith a non-heading element must includearia-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 optional 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 sub-tickets (095.A–095.Z) to add
R-subparts:sections to every existing component spec. - Implement the runtime
overridesprop inkoder_kitfirst (Flutter — largest consumer surface). Web/SDK/Native 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 per-component 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-05-22) |
| Ratification | pending |
| Implementation backlog | tools/design-gen#095 + per-component sub-tickets |
Referências
tools/design-gen/backlog/pending/095-spec-overrides-api.kmdhttps://baseweb.design/getting-started/overrides/meta/docs/stack/specs/themes/verge.kmdmeta/docs/stack/policies/reuse-first.kmd