ADRs
ADR 0044 — Component variant API UX
  • Date: 2026-06-06
  • Status: Accepted
  • Phase / Feature: Foundation v1 · in-app token-editor surface · variant editing
  • Closes: ADR 0036 Q2 — "Component variant API UX (dropdown/slider/picker)"
  • Builds on: ADR 0036 §3 (50-component library, default-described), ADR 0035 §2 (default-described components)

Context

Every Foundation component carries variants — Button has variant: 'primary'|'secondary'|'ghost'|'destructive', size: 'sm'|'md'|'lg', tone: 'default'|'success'|'danger', disabled: boolean. Modal has size, closeOnEsc, closeOnBackdropClick. Toast has placement: 'top-right'|'top-center'|…, duration: number, tone. Across the 50-component library that's ~250 variant slots a designer can edit.

ADR 0036 §3 fixed the variant shape (enum string union or boolean) but Q2 parked the in-editor UX: should variant: 'primary'|'secondary' render as a dropdown, a radio segmented control, a slider (for ordered enums like sizes), or a swatch picker (for color-bound enums)?

Picking ad-hoc per component multiplies the editor's surface area by 250 — every component author re-invents the wheel. Picking one control for everything makes simple-enum cases verbose and ordered-enum cases unreadable.

This ADR settles a mapping from variant kind to editor control. Component authors don't choose; the editor walks the variant schema and picks. New variant types added in v2 extend the table.

Decision

§1 — Variant kinds + control mapping

Every variant slot is one of six kinds. The editor picks the control from the kind.

KindSchema shapeEditor controlExample
enum-segmentedstring union, ≤4 options, options carry visual weightRadio segmented control (pill row)`variant: 'primary'
enum-dropdownstring union, 5+ options, unorderedDropdown menu`placement: 'top-right'
enum-orderedstring union with natural order, 3–5 optionsSlider with labeled stops`size: 'sm'
boolbooleanSwitch (not checkbox)disabled, closeOnEsc
number-boundednumber with min/max + stepSlider with numeric input on the sideduration: number in 0–10000 ms, step 500
token-refstring keyed to a Foundation token namespace (e.g. color.*, space.*)Token picker (swatch grid for colors, radio for sizes)tone: ColorTokenRef, gap: SpaceTokenRef

§2 — Component author surface

Each Foundation component exports a meta const consumed by the editor:

export const ButtonMeta = {
  variants: {
    variant:   { kind: "enum-segmented", options: ["primary", "secondary", "ghost", "destructive"] },
    size:      { kind: "enum-ordered",   options: ["sm", "md", "lg"] },
    disabled:  { kind: "bool" },
    leftIcon:  { kind: "token-ref",      namespace: "icon" },
  },
} as const;

The editor reads ButtonMeta.variants and renders one row per entry, picking the control from kind. No per-component UI code is written in the editor; the meta is the contract.

Component author rules:

  • Pick the kind before naming the variant. A "size" prop is enum-ordered (sm/md/lg has natural order). A "variant" prop is enum-segmented (≤4 distinct visual styles). A "placement" prop is enum-dropdown (8 corner combos, unordered).
  • ≤4 options → segmented; 5+ → dropdown. No overlap zone. If the variant has 4 today and grows to 6 later, the editor flips control automatically on the meta change.
  • Boolean is a Switch, never a checkbox. Checkbox is for multi-select; component props are binary states.

§3 — Live preview pane

To the right of every variant row, the editor shows a live preview chip rendering the component instance at the current variant values. Changing a control updates the chip immediately (debounced 100ms — variant changes don't need to re-validate WCAG, that lives at the token edit layer).

For booleans, the chip shows the toggled-on state when the switch is on. For enum-ordered, sliding shows interpolation between stops (not real interpolation — discrete jumps with a smooth animation). For enum-dropdown, the preview chip is small (~32×32) since the user is choosing from a long list, not comparing visually.

§4 — Reset to default

Every row has a single-icon reset affordance: click → variant reverts to the component's documented default (as declared in the same meta block). Useful when a designer scrolled through 12 placements and forgot the original.

§5 — Multi-instance editing (parking)

When an editor surface lands that lets a designer select multiple component instances and edit them together, the UX is: shared variants stay editable in the side panel; mismatched variants show a "Mixed" indicator and clicking the row opens a chooser ("Apply 'primary' to all 4 instances?"). Deferred to that ADR.

Anti-patterns explicit

  • Do not invent per-component controls. Every variant kind already has a mapping; if a new variant doesn't fit, extend the table (and the editor) — don't ship a one-off widget.
  • Do not use a dropdown for ≤4 options. The user pays an extra click for no information density. Segmented control wins.
  • Do not use radio buttons (vertical, separate) for variants. Segmented control is denser and keeps the variant set scannable in one row.
  • Do not debounce variant changes longer than 100ms. Variant editing is the tactile loop; perceptible lag is the worst feedback.
  • Do not show a "validate" button on the variant panel. Variant choices can't break WCAG — those live at the token edit layer (ADR 0036 §7).

Open questions / parking

  • color variant carved out of token-ref. A tone: 'default'|'danger'|'success' enum maps to color tokens internally but the designer thinks of it as a semantic enum, not a swatch pick. Keep tone as enum-segmented; introduce color: ColorTokenRef separately when components ship per-instance brand-color overrides.
  • Slider for enum-ordered accessibility. Sliders are an a11y trap historically. The implementation uses a segmented control under the hood with role="radiogroup" and the slider visual treatment — keyboard nav (arrow keys) works the same as the segmented control.
  • Live preview chip rendering for components with required children (Modal, Drawer). The chip shows a frozen "Demo content" string; expansion to real preview requires a separate canvas which is the eventual editor surface concern.