@hegemonart/get-design-done 1.14.8 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.claude-plugin/marketplace.json +5 -3
  2. package/.claude-plugin/plugin.json +15 -5
  3. package/CHANGELOG.md +97 -0
  4. package/README.md +30 -0
  5. package/SKILL.md +4 -1
  6. package/agents/a11y-mapper.md +25 -0
  7. package/agents/component-benchmark-harvester.md +112 -0
  8. package/agents/component-benchmark-synthesizer.md +88 -0
  9. package/agents/design-auditor.md +92 -8
  10. package/agents/design-context-builder.md +6 -0
  11. package/agents/design-executor.md +5 -2
  12. package/agents/design-pattern-mapper.md +2 -0
  13. package/agents/design-verifier.md +11 -0
  14. package/agents/motion-mapper.md +45 -0
  15. package/agents/token-mapper.md +36 -0
  16. package/agents/visual-hierarchy-mapper.md +29 -0
  17. package/connections/design-corpora.md +158 -0
  18. package/package.json +16 -2
  19. package/reference/anti-patterns.md +69 -0
  20. package/reference/audit-scoring.md +34 -3
  21. package/reference/brand-voice.md +199 -0
  22. package/reference/checklists.md +30 -3
  23. package/reference/components/README.md +90 -0
  24. package/reference/components/TEMPLATE.md +184 -0
  25. package/reference/components/accordion.md +217 -0
  26. package/reference/components/button.md +195 -0
  27. package/reference/components/card.md +200 -0
  28. package/reference/components/checkbox.md +207 -0
  29. package/reference/components/drawer.md +201 -0
  30. package/reference/components/input.md +208 -0
  31. package/reference/components/label.md +200 -0
  32. package/reference/components/link.md +193 -0
  33. package/reference/components/modal-dialog.md +210 -0
  34. package/reference/components/popover.md +197 -0
  35. package/reference/components/radio.md +203 -0
  36. package/reference/components/select-combobox.md +219 -0
  37. package/reference/components/switch.md +194 -0
  38. package/reference/components/tabs.md +213 -0
  39. package/reference/components/tooltip.md +201 -0
  40. package/reference/data/google-fonts.csv +51 -0
  41. package/reference/data/palettes.csv +41 -0
  42. package/reference/data/styles.csv +39 -0
  43. package/reference/design-system-guidance.md +177 -0
  44. package/reference/design-systems-catalog.md +151 -0
  45. package/reference/framer-motion-patterns.md +411 -0
  46. package/reference/gestalt.md +219 -0
  47. package/reference/iconography.md +231 -0
  48. package/reference/motion.md +102 -0
  49. package/reference/palette-catalog.md +82 -0
  50. package/reference/performance.md +304 -0
  51. package/reference/registry.json +359 -28
  52. package/reference/registry.schema.json +2 -1
  53. package/reference/review-format.md +2 -2
  54. package/reference/style-vocabulary.md +62 -0
  55. package/reference/surfaces.md +114 -0
  56. package/reference/typography.md +80 -0
  57. package/reference/visual-hierarchy-layout.md +306 -0
  58. package/skills/benchmark/SKILL.md +105 -0
@@ -0,0 +1,210 @@
1
+ # Modal / Dialog — Benchmark Spec
2
+
3
+ **Harvested from**: Radix UI Dialog, WAI-ARIA APG, Material 3, Atlassian, Carbon, Mantine, shadcn/ui, Fluent 2
4
+ **Wave**: 2 · **Category**: Containers
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+
10
+ A modal dialog is a blocking overlay that requires user interaction before returning to the main content. It is rendered in a portal above the page, traps keyboard focus within itself, prevents interaction with the background, and closes on Escape. Use modals sparingly — they interrupt flow. Prefer inline feedback or slide-out drawers for non-critical workflows. *(Material 3, Atlassian, Polaris all advise modal restraint)*
11
+
12
+ ---
13
+
14
+ ## Anatomy
15
+
16
+ ```
17
+ ┌─ Backdrop (aria-hidden) ──────────────────────────────────┐
18
+ │ │
19
+ │ ┌─ Dialog (role="dialog") ───────────────────────────┐ │
20
+ │ │ Title (aria-labelledby) [✕ Close] │ │
21
+ │ │─────────────────────────────────────────────────────│ │
22
+ │ │ Content / Body │ │
23
+ │ │─────────────────────────────────────────────────────│ │
24
+ │ │ [Cancel] [Confirm action] ← action footer │ │
25
+ │ └─────────────────────────────────────────────────────┘ │
26
+ └────────────────────────────────────────────────────────────┘
27
+ ```
28
+
29
+ | Part | Required | Notes |
30
+ |------|----------|-------|
31
+ | Backdrop | Yes | `aria-hidden="true"` overlay; blocks pointer |
32
+ | Dialog container | Yes | `role="dialog"` + `aria-modal="true"` |
33
+ | Title / heading | Yes | `id` referenced by `aria-labelledby` on dialog |
34
+ | Close button | Yes | Keyboard accessible; returns focus to trigger |
35
+ | Body content | Yes | Scrollable if content exceeds viewport |
36
+ | Action footer | Conditional | Confirm + cancel pattern |
37
+ | Portal | Yes | Rendered outside normal DOM flow; `document.body` target |
38
+
39
+ ---
40
+
41
+ ## Variants
42
+
43
+ | Variant | Description | Systems |
44
+ |---------|-------------|---------|
45
+ | Default | Centered, backdrop, standard size | All |
46
+ | Alert dialog | Blocking confirmation; `role="alertdialog"` | WAI-ARIA APG, Material 3, Carbon |
47
+ | Full-page | Mobile-first; occupies full viewport | Material 3, Atlassian |
48
+ | Small / confirm | Narrow; 2-button pattern | Material 3, Carbon, shadcn |
49
+ | Large / content | Wide; for complex forms or media | Atlassian, Fluent |
50
+ | Scrollable content | Body scrolls; header/footer sticky | All |
51
+
52
+ **Norm** (≥6/18): Escape closes; backdrop click may close (configurable); focus trapped inside.
53
+ **Diverge**: backdrop-click-to-close — Material 3 and shadcn default to close; Atlassian and Carbon recommend NOT closing on backdrop click to prevent accidental dismissal of forms.
54
+
55
+ ---
56
+
57
+ ## States
58
+
59
+ | State | ARIA |
60
+ |-------|------|
61
+ | Open | `aria-modal="true"` on dialog; `inert` on `<body>` content (or equivalent) |
62
+ | Closed | Dialog removed from DOM or `display:none`; focus returned to trigger |
63
+ | Loading content | `aria-busy="true"` on dialog body |
64
+
65
+ ---
66
+
67
+ ## Sizing & Spacing
68
+
69
+ | Size | Width | Max height | Notes |
70
+ |------|-------|------------|-------|
71
+ | sm | 400px | 80vh | Confirm dialogs |
72
+ | md (default) | 560px | 80vh | Standard |
73
+ | lg | 720px | 90vh | Complex forms |
74
+ | full | 100vw/100vh | — | Mobile sheet pattern |
75
+
76
+ Padding: 24px (header/footer), 24px (body horizontal), 20px (body vertical).
77
+ Body scroll: `overflow-y: auto` with `overscroll-behavior: contain`.
78
+
79
+ Cross-link: `reference/surfaces.md` — concentric radius, elevation (shadow-as-border)
80
+
81
+ ---
82
+
83
+ ## Typography
84
+
85
+ - Title: 18–20px/600; `id` attribute set for `aria-labelledby`
86
+ - Body: 14–16px/400
87
+ - Description (if separate from body): 14px/400 muted; linked via `aria-describedby` on dialog
88
+
89
+ ---
90
+
91
+ ## Keyboard & Accessibility
92
+
93
+ > **WAI-ARIA role**: `dialog` (or `alertdialog` for blocking confirmations)
94
+ > **Required attributes**: `aria-modal="true"`, `aria-labelledby` (dialog title id), optionally `aria-describedby`
95
+
96
+ ### Keyboard Contract
97
+
98
+ *Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/ — W3C — 2024*
99
+
100
+ | Key | Action |
101
+ |-----|--------|
102
+ | Tab | Moves focus to next focusable element inside dialog (wraps from last to first) |
103
+ | Shift+Tab | Moves focus to previous focusable element inside dialog (wraps from first to last) |
104
+ | Escape | Closes the dialog and returns focus to the element that opened it |
105
+
106
+ ### Focus Management
107
+
108
+ 1. **On open**: focus moves to the first focusable element inside the dialog (or to the dialog itself if no focusable children)
109
+ 2. **While open**: Tab/Shift+Tab cycle only within the dialog — focus MUST NOT leave the dialog
110
+ 3. **On close**: focus MUST return to the element that triggered the dialog open
111
+
112
+ ### Accessibility Rules
113
+
114
+ - `aria-modal="true"` MUST be set — tells AT to ignore background content (supplement with `inert` attribute on background for browsers without full `aria-modal` support)
115
+ - Dialog title MUST have an `id` referenced by `aria-labelledby` — screen reader announces "Dialog: [title]" on open
116
+ - `role="alertdialog"` for confirmation dialogs where the user must respond (delete confirmations, logout confirmation)
117
+ - Scroll-lock: prevent `<body>` scroll when dialog is open (`overflow: hidden` on `<body>`)
118
+ - Portal: render dialog in `document.body` to escape stacking context issues (z-index isolation)
119
+
120
+ ---
121
+
122
+ ## Motion
123
+
124
+ | Transition | Duration | Easing | Notes |
125
+ |------------|----------|--------|-------|
126
+ | Backdrop fade in | 200ms | ease-out | opacity 0→0.5 |
127
+ | Dialog enter | 200ms | ease-out | scale 0.95→1 + fade |
128
+ | Dialog exit | 150ms | ease-in | scale 1→0.95 + fade |
129
+ | Backdrop fade out | 150ms | ease-in | — |
130
+
131
+ Use `AnimatePresence` (Framer Motion) or `data-state` + CSS for mount/unmount animation.
132
+ Cross-link: `reference/motion.md` — `AnimatePresence initial={false}`, `prefers-reduced-motion`
133
+
134
+ ---
135
+
136
+ ## Do / Don't
137
+
138
+ ### Do
139
+ - Return focus to the triggering element on close *(WAI-ARIA APG, all systems)*
140
+ - Trap focus inside the dialog while open *(WAI-ARIA APG)*
141
+ - Render in a portal at `document.body` *(Radix, Mantine, shadcn)*
142
+ - Set `overflow:hidden` on `<body>` to prevent background scroll *(Material 3, Carbon)*
143
+
144
+ ### Don't
145
+ - Don't close on backdrop click for dialogs with form input — data loss risk *(Atlassian, Carbon)*
146
+ - Don't use `role="dialog"` without `aria-labelledby` — dialog is announced without a name *(WAI-ARIA APG)*
147
+ - Don't use `display:none` to hide a dialog — use DOM removal or `hidden` attribute for correct AT behavior *(WAI-ARIA APG)*
148
+ - Don't stack more than 2 dialogs — use a single dialog with internal step navigation *(Material 3, Atlassian)*
149
+
150
+ ---
151
+
152
+ ## Anti-patterns Cross-links
153
+
154
+ | Anti-pattern | Entry |
155
+ |--------------|-------|
156
+ | Focus not trapped in modal | `reference/anti-patterns.md` |
157
+ | No focus return on close | `reference/anti-patterns.md` |
158
+
159
+ ---
160
+
161
+ ## Benchmark Citations
162
+
163
+ | Claim | Sources |
164
+ |-------|---------|
165
+ | Escape closes dialog | WAI-ARIA APG §4.1, all 8 systems |
166
+ | Focus trap (Tab wraps inside) | WAI-ARIA APG §4.1 |
167
+ | aria-modal="true" required | WAI-ARIA APG |
168
+ | Portal at document.body | Radix, Mantine, shadcn |
169
+ | role="alertdialog" for confirmations | WAI-ARIA APG, Material 3 |
170
+ | backdrop-click: configurable | Material 3, shadcn (default: close); Atlassian, Carbon (default: stay) |
171
+
172
+ Full system URLs: `connections/design-corpora.md`
173
+
174
+ ---
175
+
176
+ ## Grep Signatures
177
+
178
+ ```bash
179
+ # Dialog without aria-labelledby
180
+ grep -rn 'role="dialog"\|role="alertdialog"' src/ | grep -v 'aria-labelledby'
181
+
182
+ # Dialog missing aria-modal
183
+ grep -rn 'role="dialog"' src/ | grep -v 'aria-modal'
184
+
185
+ # Modal without focus trap
186
+ grep -rn 'modal\|dialog' src/ | grep -L 'FocusTrap\|useFocusTrap\|focus-trap\|inert'
187
+
188
+ # Body scroll not locked on modal open
189
+ grep -rn 'modal.*open\|isOpen.*modal' src/ | grep -v 'overflow\|scroll-lock\|body\.'
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Failing Example
195
+
196
+ ```html
197
+ <!-- BAD: dialog with no focus trap, no aria-modal, no aria-labelledby -->
198
+ <div class="modal" style="display:block">
199
+ <div class="modal-content">
200
+ <h2>Confirm deletion</h2>
201
+ <p>This action cannot be undone.</p>
202
+ <button onclick="close()">Cancel</button>
203
+ <button onclick="confirm()">Delete</button>
204
+ </div>
205
+ </div>
206
+ ```
207
+
208
+ **Why it fails**: No `role="dialog"`, no `aria-modal`, no `aria-labelledby` — screen readers cannot announce the dialog name or suppress background content. Tab escapes the modal. Escape does nothing.
209
+ **Grep detection**: `grep -rn 'class.*modal\|class.*dialog' src/ | grep -v 'role=\|aria-'`
210
+ **Fix**: Use Radix `<Dialog>` or implement WAI-ARIA dialog pattern with `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, focus trap, Escape handler, and portal rendering.
@@ -0,0 +1,197 @@
1
+ # Popover — Benchmark Spec
2
+
3
+ **Harvested from**: Radix UI Popover, Floating UI, Mantine, Atlassian, WAI-ARIA APG, shadcn/ui, Carbon, Material 3
4
+ **Wave**: 2 · **Category**: Containers
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+
10
+ A popover is an anchored overlay that appears beside a trigger element, containing richer content than a tooltip (interactive content, forms, menus). It is dismissed by clicking outside, pressing Escape, or activating a close button. Unlike a modal, it does not trap focus (unless it contains a form that requires isolation). *(Radix, Floating UI, WAI-ARIA APG differentiate popover from both tooltip and modal)*
11
+
12
+ ---
13
+
14
+ ## Anatomy
15
+
16
+ ```
17
+ [Trigger button]
18
+
19
+ ▼ (arrow / caret — optional)
20
+ ┌───────────────────────┐
21
+ │ Title (opt.) [✕] │ ← optional close button
22
+ │───────────────────────│
23
+ │ Content / form │
24
+ │ │
25
+ │ [Action] │
26
+ └───────────────────────┘
27
+ ```
28
+
29
+ | Part | Required | Notes |
30
+ |------|----------|-------|
31
+ | Trigger | Yes | Button or interactive element that opens the popover |
32
+ | Popover container | Yes | `role="dialog"` OR no role for non-modal content |
33
+ | Content | Yes | Can include interactive elements (forms, links, buttons) |
34
+ | Arrow pointer | No | 8–12px; indicates anchor relationship |
35
+ | Close button | No | When popover contains a form (escape is always active) |
36
+ | Backdrop | No | Popovers typically dismiss on outside-click, not backdrop |
37
+
38
+ ---
39
+
40
+ ## Variants
41
+
42
+ | Variant | Description | Systems |
43
+ |---------|-------------|---------|
44
+ | Default | Anchored to trigger; dismisses on outside-click | All |
45
+ | With title | Title bar + close button for complex content | Radix, shadcn, Atlassian |
46
+ | Form popover | Contains inputs; focus trap optional | Radix, Mantine |
47
+ | Contextual menu | List of actions (see also: Dropdown Menu pattern) | Material 3, Carbon |
48
+ | Inline picker | Date, color, emoji picker | Material 3, Mantine |
49
+
50
+ **Norm** (≥5/18): Escape closes; outside-click closes; arrow pointer indicates anchor.
51
+ **Diverge**: focus trap — Radix Popover does NOT trap focus (non-modal); Radix Dialog DOES. Use Dialog-pattern when content isolation is required.
52
+
53
+ ---
54
+
55
+ ## States
56
+
57
+ | State | ARIA |
58
+ |-------|------|
59
+ | Closed | `aria-expanded="false"` on trigger |
60
+ | Open | `aria-expanded="true"` on trigger; `aria-controls="popover-id"` |
61
+ | Loading | `aria-busy="true"` on popover body |
62
+
63
+ ---
64
+
65
+ ## Positioning
66
+
67
+ *Per Floating UI — https://floating-ui.com — MIT — 2024*
68
+
69
+ | Property | Recommended Value | Notes |
70
+ |----------|-------------------|-------|
71
+ | Placement | `bottom` (default), `top`, `left`, `right` | Auto-flip on viewport edge |
72
+ | Auto-flip | `flip()` middleware | Flips to opposite side when space is insufficient |
73
+ | Auto-shift | `shift()` middleware | Shifts along the axis to stay in viewport |
74
+ | Offset | 8px from trigger | Gap between trigger and popover edge |
75
+ | Arrow | `arrow()` middleware | CSS custom property `--arrow-x`, `--arrow-y` |
76
+
77
+ Positioning must update on scroll and resize (`autoUpdate` from Floating UI).
78
+
79
+ ---
80
+
81
+ ## Sizing & Spacing
82
+
83
+ | Property | Value | Notes |
84
+ |----------|-------|-------|
85
+ | Min width | 200px | Prevents content collapse |
86
+ | Max width | 360px | Wider → use modal instead |
87
+ | Padding | 16px | |
88
+ | Arrow size | 8×8px | |
89
+ | Border radius | Match design token | `reference/surfaces.md` |
90
+
91
+ ---
92
+
93
+ ## Keyboard & Accessibility
94
+
95
+ > **WAI-ARIA role**: `dialog` (when content requires focus isolation) OR no specific role
96
+ > **Trigger attributes**: `aria-expanded`, `aria-controls` (popover id), `aria-haspopup="dialog"` (when role="dialog")
97
+
98
+ ### Keyboard Contract
99
+
100
+ *Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/ (non-modal variant) — W3C — 2024*
101
+
102
+ | Key | Action |
103
+ |-----|--------|
104
+ | Escape | Closes the popover; returns focus to trigger |
105
+ | Tab (inside popover) | Moves focus through interactive elements inside popover |
106
+ | Tab (last element inside) | Closes popover; moves focus to next element after trigger |
107
+
108
+ ### Accessibility Rules
109
+
110
+ - Trigger MUST have `aria-expanded` toggled on open/close
111
+ - Trigger MUST have `aria-controls` pointing to the popover's `id`
112
+ - When popover is dismissed, focus MUST return to the trigger element
113
+ - Non-modal popover: Tab MAY leave the popover (focus is not trapped)
114
+ - Modal popover (form isolation): add `role="dialog"` + `aria-modal="true"` + focus trap
115
+ - Popover with interactive content: do NOT use `role="tooltip"` — tooltip cannot contain interactive elements
116
+
117
+ ---
118
+
119
+ ## Motion
120
+
121
+ | Transition | Duration | Easing | Notes |
122
+ |------------|----------|--------|-------|
123
+ | Open | 120ms | ease-out | scale 0.95→1 + fade; origin at trigger |
124
+ | Close | 80ms | ease-in | fade only |
125
+ | Position update | 0ms | — | No animation on reposition (prevents jank on scroll) |
126
+
127
+ Cross-link: `reference/motion.md` — `AnimatePresence`, `data-state` trigger for CSS transitions
128
+
129
+ ---
130
+
131
+ ## Do / Don't
132
+
133
+ ### Do
134
+ - Use Floating UI or equivalent for positioning — manual positioning breaks on scroll and viewport edge *(Radix, Mantine, shadcn)*
135
+ - Dismiss on outside-click AND Escape *(WAI-ARIA APG, Radix, all systems)*
136
+ - Return focus to trigger on close *(WAI-ARIA APG)*
137
+ - Auto-flip and auto-shift so popover stays in viewport *(Floating UI)*
138
+
139
+ ### Don't
140
+ - Don't use `role="tooltip"` for popovers with interactive content — tooltip has a different contract *(WAI-ARIA APG)*
141
+ - Don't position with `position: absolute` without a Floating UI — it will misalign on scroll *(Floating UI docs)*
142
+ - Don't make popovers wider than 360px — use a modal for complex content *(Atlassian, Carbon)*
143
+ - Don't auto-open popovers on hover — use tooltip for hover-triggered content *(Radix, WAI-ARIA APG)*
144
+
145
+ ---
146
+
147
+ ## Anti-patterns Cross-links
148
+
149
+ | Anti-pattern | Entry |
150
+ |--------------|-------|
151
+ | role="tooltip" on interactive content | `reference/anti-patterns.md` |
152
+ | Manual absolute positioning | `reference/anti-patterns.md` |
153
+
154
+ ---
155
+
156
+ ## Benchmark Citations
157
+
158
+ | Claim | Sources |
159
+ |-------|---------|
160
+ | Escape closes popover | WAI-ARIA APG, Radix, all systems |
161
+ | aria-expanded on trigger | WAI-ARIA APG |
162
+ | Floating UI for positioning | Radix, Mantine, shadcn |
163
+ | No focus trap (non-modal) | Radix Popover docs |
164
+ | 8px offset from trigger | Floating UI default |
165
+
166
+ Full system URLs: `connections/design-corpora.md`
167
+
168
+ ---
169
+
170
+ ## Grep Signatures
171
+
172
+ ```bash
173
+ # Popover trigger missing aria-expanded
174
+ grep -rn 'popover\|Popover' src/ | grep 'trigger\|button' | grep -v 'aria-expanded'
175
+
176
+ # role="tooltip" used for interactive popover
177
+ grep -rn 'role="tooltip"' src/ | xargs grep -l 'button\|input\|<a ' 2>/dev/null
178
+
179
+ # Manual absolute positioning (no Floating UI)
180
+ grep -rn 'position:\s*absolute' src/ | grep -i 'popover\|dropdown\|overlay'
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Failing Example
186
+
187
+ ```html
188
+ <!-- BAD: popover with role="tooltip" containing a button — wrong role, keyboard broken -->
189
+ <div role="tooltip" id="popover" style="position:absolute;top:40px">
190
+ <p>Quick actions</p>
191
+ <button onclick="doAction()">Apply</button>
192
+ </div>
193
+ ```
194
+
195
+ **Why it fails**: `role="tooltip"` cannot contain interactive elements per ARIA spec. The button is announced incorrectly. Escape may not close (tooltip close behavior differs from popover/dialog).
196
+ **Grep detection**: `grep -rn 'role="tooltip"' src/ | xargs grep -l 'button\|<a \|input' 2>/dev/null`
197
+ **Fix**: Use `role="dialog"` with `aria-modal="false"` (non-modal) for interactive popovers, or use Radix `<Popover>` which handles all ARIA and positioning automatically.
@@ -0,0 +1,203 @@
1
+ # Radio — Benchmark Spec
2
+
3
+ **Harvested from**: Material 3, Carbon, Fluent 2, Mantine, WAI-ARIA APG, Polaris, Ant Design, Chakra UI
4
+ **Wave**: 1 · **Category**: Inputs
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+
10
+ A radio button represents one option within a mutually exclusive group. Selecting one radio button deselects all others in the same group. Always wrap radio buttons in a `<fieldset>` + `<legend>` and group them with the same `name` attribute. Never use a single radio button — it creates an unresettable state. Use a checkbox for binary choices.
11
+
12
+ ---
13
+
14
+ ## Anatomy
15
+
16
+ ```
17
+ <fieldset>
18
+ <legend>Shipping method</legend>
19
+
20
+ ( ) Standard shipping ← <input type="radio" name="shipping" id="standard">
21
+ (●) Express shipping ← <input type="radio" name="shipping" id="express" checked>
22
+ ( ) Overnight ← <input type="radio" name="shipping" id="overnight">
23
+ </fieldset>
24
+ ```
25
+
26
+ | Part | Required | Notes |
27
+ |------|----------|-------|
28
+ | `<fieldset>` | Yes | Groups the radio set |
29
+ | `<legend>` | Yes | Names the group; read by screen readers before each option |
30
+ | `<input type="radio">` | Yes | Native element; same `name` within group |
31
+ | `<label for="id">` | Yes | Click zone extends to label text |
32
+ | Helper text | No | Below individual item or below legend |
33
+ | Error message | Conditional | Group-level error via `aria-describedby` on `<fieldset>` |
34
+
35
+ ---
36
+
37
+ ## Variants
38
+
39
+ | Variant | Description | Systems |
40
+ |---------|-------------|---------|
41
+ | Default (stacked) | Vertical list of options | All |
42
+ | Horizontal | Side-by-side for short labels (2–3 options) | Material 3, Carbon, Mantine |
43
+ | Radio card | Clickable card with radio indicator | Polaris, Mantine, shadcn |
44
+ | Button group | Segmented-control appearance | Material 3, Fluent 2, Carbon |
45
+
46
+ **Norm** (≥6/18): vertical stacking default; horizontal allowed for ≤3 short labels.
47
+ **Diverge**: button-group vs. radio-card — visual styling differs but keyboard contract is identical.
48
+
49
+ ---
50
+
51
+ ## States
52
+
53
+ | State | Trigger | Visual | ARIA |
54
+ |-------|---------|--------|------|
55
+ | unselected | default | Empty circle | — |
56
+ | selected | user interaction / programmatic | Filled dot | `checked` attr |
57
+ | hover | pointer over | Circle border darkens | — |
58
+ | focus | keyboard (Tab into group) | 2px focus ring on currently selected / first | — |
59
+ | disabled | `disabled` attr | 38% opacity | `aria-disabled="true"` |
60
+ | error | group-level | Red border on legend/container | `aria-describedby` on `<fieldset>` |
61
+
62
+ ---
63
+
64
+ ## Sizing & Spacing
65
+
66
+ | Property | Value | Notes |
67
+ |----------|-------|-------|
68
+ | Control diameter | 16–20px | *(Material 3: 20px, Carbon: 16px, Polaris: 16px)* |
69
+ | Dot diameter | 8–10px (center of control) | |
70
+ | Gap: control → label | 8px | |
71
+ | Item spacing (vertical) | 8–12px | *(Carbon: 8px, Material 3: 8px)* |
72
+ | Touch target | 44×44px via pseudo-element | `reference/surfaces.md` |
73
+
74
+ Cross-link: `reference/surfaces.md` — hit-area pseudo-element pattern
75
+
76
+ ---
77
+
78
+ ## Typography
79
+
80
+ - Label: 14px/400 — same as body; option is a choice, not a heading
81
+ - Legend: 14px/500 or 12px/600 uppercase — distinct from option labels
82
+ - Helper/error: 12px/400
83
+
84
+ ---
85
+
86
+ ## Keyboard & Accessibility
87
+
88
+ > **WAI-ARIA role**: `radio` (implicit on `<input type="radio">`)
89
+ > **Group role**: `radiogroup` (via `<fieldset>` + ARIA, or `role="radiogroup"` explicitly)
90
+ > **Required attributes**: `name` (groups radios), `id` + `<label for>` per item
91
+
92
+ ### Keyboard Contract
93
+
94
+ *Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/radio/ — W3C — 2024*
95
+
96
+ | Key | Action |
97
+ |-----|--------|
98
+ | Tab | Moves focus into group (to the checked radio, or first if none checked) |
99
+ | Tab (from within group) | Moves focus out of group to next focusable element |
100
+ | Arrow Right / Arrow Down | Moves focus to next radio AND selects it |
101
+ | Arrow Left / Arrow Up | Moves focus to previous radio AND selects it |
102
+ | Space | If the focused radio is not selected, selects it |
103
+
104
+ **Key insight**: Arrow keys move AND select simultaneously (auto-advance). This differs from checkboxes (Space toggles, Tab moves). Tab navigates in/out of the whole group as one focusable unit.
105
+
106
+ ### Accessibility Rules
107
+
108
+ - `<fieldset>` + `<legend>` MUST wrap the entire group — legend text is prepended to each option announcement
109
+ - All radios in a group MUST share the same `name` attribute
110
+ - Tab focuses the selected radio (or first, if none selected) — arrow keys navigate within the group
111
+ - Never use a single radio button — it creates a state the user cannot unset; use a checkbox
112
+ - Error state applies to the group: `aria-describedby` on the `<fieldset>` or `role="radiogroup"` container
113
+ - `required`: apply to all radios in the group or use `aria-required` on the group container
114
+
115
+ ---
116
+
117
+ ## Motion
118
+
119
+ | Transition | Duration | Easing | Notes |
120
+ |------------|----------|--------|-------|
121
+ | Dot fill | 120ms | ease-out | Scale 0→1 from center |
122
+ | Deselect | 80ms | ease | Dot scale 1→0 |
123
+ | Hover border | 80ms | ease | Border colour only |
124
+
125
+ Cross-link: `reference/motion.md` — `prefers-reduced-motion`: skip dot animation, show fill instantly
126
+
127
+ ---
128
+
129
+ ## Do / Don't
130
+
131
+ ### Do
132
+ - Always use `<fieldset>` + `<legend>` for the group *(WAI-ARIA APG, Carbon, Polaris)*
133
+ - Use the same `name` attribute for all radios in a group *(HTML spec, all systems)*
134
+ - Pre-select a default option where appropriate to reduce cognitive load *(Material 3, Polaris)*
135
+ - Use arrow keys to navigate within a group — Tab moves to the group, not each item *(WAI-ARIA APG)*
136
+
137
+ ### Don't
138
+ - Don't use a single radio button — use a checkbox instead *(Material 3, Carbon, WAI-ARIA APG)*
139
+ - Don't use radio buttons for mutually exclusive options that require confirmation — use a select *(Polaris)*
140
+ - Don't mix radio and checkbox styles in the same group *(Carbon, Polaris)*
141
+ - Don't require the user to make a selection before seeing other page content (premature required state) *(Polaris)*
142
+
143
+ ---
144
+
145
+ ## Anti-patterns Cross-links
146
+
147
+ | Anti-pattern | Entry |
148
+ |--------------|-------|
149
+ | Lone radio button | `reference/anti-patterns.md` |
150
+
151
+ ---
152
+
153
+ ## Benchmark Citations
154
+
155
+ | Claim | Sources |
156
+ |-------|---------|
157
+ | Arrow keys move + select simultaneously | WAI-ARIA APG §3.6 |
158
+ | Tab focuses whole group as one unit | WAI-ARIA APG |
159
+ | fieldset+legend required | WAI-ARIA APG, Carbon, Polaris |
160
+ | Single radio = anti-pattern | Material 3, Carbon, WAI-ARIA APG |
161
+
162
+ Full system URLs: `connections/design-corpora.md`
163
+
164
+ ---
165
+
166
+ ## Grep Signatures
167
+
168
+ ```bash
169
+ # Single radio button (no group = unresettable state)
170
+ grep -rn 'type="radio"' src/ | sort | uniq -c | sort -n | head
171
+
172
+ # Radio group missing name attribute
173
+ grep -rn 'type="radio"' src/ | grep -v 'name='
174
+
175
+ # Radio group without fieldset or role="radiogroup"
176
+ grep -rn 'type="radio"' src/ | grep -v 'fieldset\|role="radiogroup"'
177
+
178
+ # Arrow key handler missing in custom radio implementation
179
+ grep -rn 'role="radio"' src/ | grep -v 'ArrowDown\|ArrowUp\|onKeyDown'
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Failing Example
185
+
186
+ ```html
187
+ <!-- BAD: radio group without fieldset/legend and with keyboard nav broken -->
188
+ <div>
189
+ <input type="radio" id="a" name="plan"> <label for="a">Starter</label>
190
+ <input type="radio" id="b" name="plan"> <label for="b">Pro</label>
191
+ </div>
192
+ ```
193
+
194
+ **Why it fails**: Screen reader announces each option without the group question. The `<div>` provides no semantic grouping; `<legend>` is not present, so users don't know what "Starter" and "Pro" refer to.
195
+ **Grep detection**: `grep -B3 'type="radio"' src/ | grep '<div' | grep -v 'fieldset'`
196
+ **Fix**:
197
+ ```html
198
+ <fieldset>
199
+ <legend>Select your plan</legend>
200
+ <input type="radio" id="a" name="plan"> <label for="a">Starter</label>
201
+ <input type="radio" id="b" name="plan"> <label for="b">Pro</label>
202
+ </fieldset>
203
+ ```