- 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.
| Kind | Schema shape | Editor control | Example |
|---|---|---|---|
enum-segmented | string union, ≤4 options, options carry visual weight | Radio segmented control (pill row) | `variant: 'primary' |
enum-dropdown | string union, 5+ options, unordered | Dropdown menu | `placement: 'top-right' |
enum-ordered | string union with natural order, 3–5 options | Slider with labeled stops | `size: 'sm' |
bool | boolean | Switch (not checkbox) | disabled, closeOnEsc |
number-bounded | number with min/max + step | Slider with numeric input on the side | duration: number in 0–10000 ms, step 500 |
token-ref | string 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 isenum-segmented(≤4 distinct visual styles). A "placement" prop isenum-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
colorvariant carved out oftoken-ref. Atone: 'default'|'danger'|'success'enum maps to color tokens internally but the designer thinks of it as a semantic enum, not a swatch pick. Keeptoneasenum-segmented; introducecolor: ColorTokenRefseparately when components ship per-instance brand-color overrides.- Slider for
enum-orderedaccessibility. Sliders are an a11y trap historically. The implementation uses a segmented control under the hood withrole="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.