Pular para o conteúdo

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

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:

  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 every KDS component a customization surface — consumers swap named subparts without forking.

R2 — Vocabulary

TermMeaning
SubpartA named, addressable region of a component. Every component has at least Root; concrete components add names like Title, Body, Icon, CloseButton, Item.
OverrideThe consumer-supplied value for a subpart.
Override modeOne of style (CSS-only delta), component (full swap), props (extra props merged into the default render).
Override mapThe 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 ❌ — use CloseButton).
  • Generic names (Element, Inner ❌ — name the role).
  • Implementation-specific names (Anchor ❌ when the real role is Link).

R6 — Per-platform implementation

SurfaceDefault 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 nativeCompose OverridesMap parameter on every Koder* composable.
iOS nativeSwift 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:

  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:

  • 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 CloseButton with a component rendering below 44 × 44 dp fails the touch-target audit (specs/a11y/touch-targets.kmd).
  • Semantic 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 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)

IDTest
T1Component renders with no overrides — golden equals baseline.
T2overrides.Title.style swaps the rendered color; golden differs.
T3overrides.Icon.component swaps the icon element; new element rendered, default element absent.
T4overrides.CloseButton.props adds the new prop; default props preserved; conflicting prop overridden.
T5Override on an unknown subpart name MUST emit a dev-mode warning + fail audit.
T6Combined modes resolve in the documented order (props → style → component).
T7A11y guard fires when a contrast-breaking style is supplied.
T8SSR renders the override identically to client (no hydration delta).

R11 — Migration plan

  1. Ship this spec (draft → ratified after owner sign-off).
  2. Open per-component sub-tickets (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). Web/SDK/Native 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 (Provider-style), or is per-component 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

RoleOwner
Author@rpm (2026-05-22)
Ratificationpending
Implementation backlogtools/design-gen#095 + per-component sub-tickets

Referências