@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,219 @@
1
+ # Select / Combobox — Benchmark Spec
2
+
3
+ **Harvested from**: Radix UI, WAI-ARIA APG, Carbon, Headless UI, Mantine, Material 3, Ant Design, shadcn/ui
4
+ **Wave**: 1 · **Category**: Inputs
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+
10
+ A select allows the user to choose one (or multiple) options from a predefined list presented in a dropdown. A combobox extends this with a text filter input. Use native `<select>` when styling flexibility is not required and options are static; use a custom combobox when filtering, grouping, async loading, or complex option rendering is needed. *(Radix, Headless UI, WAI-ARIA APG agree on this decision tree)*
11
+
12
+ ---
13
+
14
+ ## Anatomy
15
+
16
+ ```
17
+ Label *
18
+ ┌────────────────────────┬──┐
19
+ │ Selected value │ ▾│ ← trigger button (role="combobox" + aria-haspopup)
20
+ └────────────────────────┴──┘
21
+ ┌──────────────────────────┐
22
+ │ [search input] │ ← combobox only; role="textbox"
23
+ │──────────────────────────│
24
+ │ ○ Option A │ ← role="option" in role="listbox"
25
+ │ ○ Option B │
26
+ │ ○ Option C │
27
+ └──────────────────────────┘
28
+ ```
29
+
30
+ | Part | Required | Notes |
31
+ |------|----------|-------|
32
+ | Label | Yes | `<label>` or `aria-label` on trigger |
33
+ | Trigger button | Yes | Announces selected value; opens dropdown on Enter/Space |
34
+ | Dropdown list | Yes | `role="listbox"` container |
35
+ | Option items | Yes | `role="option"` with `aria-selected` |
36
+ | Filter input | No | Combobox only; `role="combobox"` with `aria-controls` |
37
+ | Group headings | No | `role="group"` with `aria-label` |
38
+ | Empty state | Conditional | Required when async/filter can return zero results |
39
+ | Clear button | No | Clears selection; keyboard accessible |
40
+
41
+ ---
42
+
43
+ ## Variants
44
+
45
+ | Variant | Description | Systems |
46
+ |---------|-------------|---------|
47
+ | Native select | `<select>` — minimal, accessible, no custom styling | All (as baseline) |
48
+ | Custom select | Styled trigger + listbox; no filter | Radix, Carbon, shadcn |
49
+ | Combobox | Trigger + text filter + listbox | Radix, Mantine, Headless UI, Ant |
50
+ | Multi-select | Multiple `aria-selected="true"` options; tag display | Mantine, Ant, Carbon |
51
+ | Async / searchable | Options loaded on query; loading state in listbox | Mantine, Ant, shadcn |
52
+
53
+ **Norm** (≥5/18): custom select must replicate native keyboard behavior exactly.
54
+ **Diverge**: tag vs. chip display for multi-select — Mantine uses tags, Ant uses chips, Carbon uses checkboxes in dropdown. Checkbox approach is most accessible (state is visible without removing focus from listbox).
55
+
56
+ ---
57
+
58
+ ## States
59
+
60
+ | State | Trigger | Visual | ARIA |
61
+ |-------|---------|--------|------|
62
+ | default | — | Resting trigger | `aria-expanded="false"` |
63
+ | open | trigger activated | Dropdown visible | `aria-expanded="true"` |
64
+ | option hovered | pointer | Option highlighted | `aria-activedescendant` updated |
65
+ | option selected | Enter / click | Checkmark or filled circle | `aria-selected="true"` |
66
+ | disabled | `disabled` attr | 38% opacity; pointer-events: none | `aria-disabled="true"` |
67
+ | error | validation | Red border + error message | `aria-invalid="true"` |
68
+ | loading | async fetch | Spinner inside listbox | `aria-busy="true"` on listbox |
69
+ | empty | filter = zero results | "No results" message in listbox | `aria-live="polite"` |
70
+
71
+ ---
72
+
73
+ ## Sizing & Spacing
74
+
75
+ | Size | Trigger height | Option height | Padding H |
76
+ |------|---------------|---------------|-----------|
77
+ | sm | 32px | 28px | 12px |
78
+ | md (default) | 40px | 36px | 16px |
79
+ | lg | 48px | 44px | 16px |
80
+
81
+ Max dropdown height: 240px with internal scroll — prevents viewport overflow without pagination.
82
+ Option min-height: 36px (md) for touch targets *(Material 3, Polaris)*
83
+
84
+ ---
85
+
86
+ ## Typography
87
+
88
+ - Trigger value: same weight/size as input (14px/400)
89
+ - Option text: 14px/400; selected option: 14px/500
90
+ - Group label: 11px/600 uppercase, muted colour — not an option, not focusable
91
+
92
+ ---
93
+
94
+ ## Keyboard & Accessibility
95
+
96
+ > **WAI-ARIA role**: `combobox` on trigger (select pattern); `listbox` on dropdown; `option` on items
97
+ > **Required attributes**: `aria-expanded`, `aria-haspopup="listbox"`, `aria-controls` (trigger→listbox), `aria-activedescendant` (updated on option focus)
98
+
99
+ ### Keyboard Contract — Select (Listbox Pattern)
100
+
101
+ *Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/listbox/ — W3C — 2024*
102
+
103
+ | Key | Action |
104
+ |-----|--------|
105
+ | Enter / Space | Opens listbox when trigger is focused |
106
+ | Escape | Closes listbox; returns focus to trigger |
107
+ | Arrow Down | Opens listbox (if closed) or moves focus to next option |
108
+ | Arrow Up | Opens listbox (if closed) or moves focus to previous option |
109
+ | Home | Moves focus to first option |
110
+ | End | Moves focus to last option |
111
+ | Enter (option focused) | Selects option; closes listbox |
112
+ | Tab | Closes listbox; moves focus to next element |
113
+ | Printable character | (Type-ahead) Moves focus to next option starting with typed character |
114
+
115
+ ### Keyboard Contract — Combobox
116
+
117
+ *Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/combobox/ — W3C — 2024*
118
+
119
+ | Key | Action |
120
+ |-----|--------|
121
+ | Any printable character | Filters option list; opens popup |
122
+ | Escape | Clears filter (if any); closes popup |
123
+ | Arrow Down | Moves focus into listbox (first or previously focused option) |
124
+ | Arrow Up | Moves focus to last option |
125
+ | Enter | Selects focused option; closes popup |
126
+ | Alt + Arrow Down | Opens popup without moving focus |
127
+ | Alt + Arrow Up | Closes popup; returns focus to textbox |
128
+
129
+ ### Accessibility Rules
130
+
131
+ - `aria-activedescendant` on trigger MUST update as options are highlighted — screen readers follow this, not DOM focus
132
+ - Options must have unique `id` attributes (for `aria-activedescendant` reference)
133
+ - Grouping: `role="group"` with `aria-label` wrapping grouped `role="option"` items
134
+ - Empty state: announce via `aria-live="polite"` region when filter returns no results
135
+ - Virtualised lists: keep rendered DOM options in sync with `aria-activedescendant` pointer
136
+
137
+ ---
138
+
139
+ ## Motion
140
+
141
+ | Transition | Duration | Easing | Notes |
142
+ |------------|----------|--------|-------|
143
+ | Dropdown open | 120ms | ease-out | Scale 0.95→1 + fade; origin at trigger |
144
+ | Dropdown close | 80ms | ease-in | Fade only |
145
+ | Option highlight | 60ms | ease | Background colour only |
146
+
147
+ Cross-link: `reference/motion.md` — `AnimatePresence` pattern for mount/unmount
148
+
149
+ ---
150
+
151
+ ## Do / Don't
152
+
153
+ ### Do
154
+ - Replicate native `<select>` keyboard behavior exactly in custom implementations *(WAI-ARIA APG, Radix)*
155
+ - Show a "No results" state (not empty dropdown) when filter has no matches *(Mantine, shadcn, Carbon)*
156
+ - Update `aria-activedescendant` on every option focus change *(WAI-ARIA APG)*
157
+ - Limit dropdown height to ~6–8 options; add scroll for more *(Carbon, Material 3)*
158
+
159
+ ### Don't
160
+ - Don't close the dropdown on every keystroke in combobox mode — only on selection or Escape *(WAI-ARIA APG)*
161
+ - Don't use a select for navigation — use a nav + router *(Carbon, Primer)*
162
+ - Don't disable scroll inside the listbox — virtualise instead for large lists *(Mantine, Ant)*
163
+ - Don't place interactive elements (links, buttons) inside `role="option"` *(WAI-ARIA APG)*
164
+
165
+ ---
166
+
167
+ ## Anti-patterns Cross-links
168
+
169
+ | Anti-pattern | Entry |
170
+ |--------------|-------|
171
+ | Custom select without ARIA | `reference/anti-patterns.md` — ARIA role omission pattern |
172
+
173
+ ---
174
+
175
+ ## Benchmark Citations
176
+
177
+ | Claim | Sources |
178
+ |-------|---------|
179
+ | aria-activedescendant pattern | WAI-ARIA APG combobox §4.1 |
180
+ | Escape closes + returns focus | WAI-ARIA APG listbox §3.4 |
181
+ | 240px max dropdown height | Carbon, Material 3 |
182
+ | Checkbox approach for multi-select a11y | Carbon, WAI-ARIA APG |
183
+
184
+ Full system URLs: `connections/design-corpora.md`
185
+
186
+ ---
187
+
188
+ ## Grep Signatures
189
+
190
+ ```bash
191
+ # Custom select/combobox missing aria-expanded
192
+ grep -rn 'role="combobox"\|role="listbox"' src/ | grep -v 'aria-expanded'
193
+
194
+ # Options missing aria-selected
195
+ grep -rn 'role="option"' src/ | grep -v 'aria-selected'
196
+
197
+ # Missing aria-activedescendant on trigger
198
+ grep -rn 'role="combobox"' src/ | grep -v 'aria-activedescendant'
199
+
200
+ # Dropdown closed on every keypress (bad UX in combobox)
201
+ grep -rn 'onKeyDown\|on:keydown' src/ | grep -i 'select\|combobox\|dropdown'
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Failing Example
207
+
208
+ ```html
209
+ <!-- BAD: custom dropdown with no ARIA — keyboard users and screen readers are stranded -->
210
+ <div class="select-trigger" onclick="toggleDropdown()">Choose option</div>
211
+ <ul class="dropdown">
212
+ <li onclick="selectOption('a')">Option A</li>
213
+ <li onclick="selectOption('b')">Option B</li>
214
+ </ul>
215
+ ```
216
+
217
+ **Why it fails**: No role, no aria-expanded, no keyboard navigation, no focus management.
218
+ **Grep detection**: `grep -rn 'class.*dropdown\|class.*select' src/ | grep -v 'role='`
219
+ **Fix**: Use Radix `<Select>` or implement WAI-ARIA listbox pattern with `role="combobox"`, `role="listbox"`, `role="option"`, `aria-expanded`, and `aria-activedescendant`.
@@ -0,0 +1,194 @@
1
+ # Switch — Benchmark Spec
2
+
3
+ **Harvested from**: Apple HIG, Material 3, Radix UI, Spectrum (Adobe), Polaris, Fluent 2, Mantine, shadcn/ui
4
+ **Wave**: 1 · **Category**: Inputs
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+
10
+ A switch (toggle) represents an immediate binary action — changes take effect without a confirm step, like enabling dark mode or activating a feature. It differs from a checkbox in this immediacy: a checkbox is a form option submitted later; a switch acts now. Do not use a switch inside a form that requires a submit button to apply changes. *(Apple HIG, Material 3, Polaris all distinguish switch from checkbox this way)*
11
+
12
+ ---
13
+
14
+ ## Anatomy
15
+
16
+ ```
17
+ Label ← visible text describing the setting
18
+ ◉─────── ON ← track + thumb; thumb slides right on ON
19
+ ○─────── OFF ← thumb left on OFF
20
+ Helper text (opt.)
21
+ ```
22
+
23
+ | Part | Required | Notes |
24
+ |------|----------|-------|
25
+ | Track | Yes | Background bar; 28–52px wide, 14–32px tall |
26
+ | Thumb | Yes | Circular indicator that slides |
27
+ | Label | Yes (or `aria-label`) | Describes what the switch controls |
28
+ | State text (ON/OFF) | No | Optional inside or beside track; not required by a11y |
29
+ | Helper text | No | Clarifies effect of the switch |
30
+
31
+ ---
32
+
33
+ ## Variants
34
+
35
+ | Variant | Description | Systems |
36
+ |---------|-------------|---------|
37
+ | Default | Label left/right, switch right/left | All |
38
+ | With icon | Icon inside thumb (check/x) | Material 3, Fluent 2 |
39
+ | With state text | ON/OFF inside track | Apple HIG (iOS), Fluent 2 |
40
+ | Small | Compact size (24px height) | Mantine, shadcn |
41
+ | Large | 32px height | Material 3, Apple HIG |
42
+
43
+ **Norm** (≥5/18): label appears to the left of the switch (LTR); toggle is right-aligned.
44
+ **Diverge**: thumb icon vs. bare thumb — Material 3 adds check icon on ON, x on OFF; most others use bare thumb. Bare is simpler and more portable across themes.
45
+
46
+ ---
47
+
48
+ ## States
49
+
50
+ | State | Visual | ARIA |
51
+ |-------|--------|------|
52
+ | OFF (unchecked) | Thumb left, track muted | `aria-checked="false"` |
53
+ | ON (checked) | Thumb right, track colored | `aria-checked="true"` |
54
+ | hover | Thumb scale up slightly (1.1×) | — |
55
+ | focus | 2px focus ring around track | — |
56
+ | disabled OFF | 38% opacity, thumb left | `aria-disabled="true"` |
57
+ | disabled ON | 38% opacity, thumb right | `aria-disabled="true"` |
58
+
59
+ ---
60
+
61
+ ## Sizing & Spacing
62
+
63
+ | Size | Track W×H | Thumb diameter | Touch target |
64
+ |------|-----------|----------------|--------------|
65
+ | sm | 36×20px | 16px | 44×44px via pseudo-element |
66
+ | md (default) | 44×24px | 20px | 44×44px |
67
+ | lg | 52×28px | 24px | 48×48px |
68
+
69
+ Track radius: `border-radius: 9999px` (pill shape — all 8 systems agree).
70
+ Thumb travel: track width − thumb diameter − (2 × inset padding ≈ 2px).
71
+
72
+ Cross-link: `reference/surfaces.md` — concentric radius rule does NOT apply here (pill shape is intentional, not nested radius)
73
+
74
+ ---
75
+
76
+ ## Typography
77
+
78
+ - Label: 14px/400 body weight; not distinguished from surrounding text
79
+ - State text (optional): 10–11px/600 uppercase inside track — ensure ≥4.5:1 contrast against track colour
80
+
81
+ ---
82
+
83
+ ## Keyboard & Accessibility
84
+
85
+ > **WAI-ARIA role**: `switch`
86
+ > **Required attributes**: `aria-checked` ("true" / "false"); `aria-label` or visible associated label
87
+
88
+ ### Keyboard Contract
89
+
90
+ *Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/switch/ — W3C — 2024*
91
+
92
+ | Key | Action |
93
+ |-----|--------|
94
+ | Space | Toggles the switch state |
95
+ | Enter | (Optional) Toggles the switch state |
96
+
97
+ ### Accessibility Rules
98
+
99
+ - `role="switch"` with `aria-checked="true/false"` — not `role="checkbox"` (different semantic contract; switch implies immediate action)
100
+ - Label MUST be associated: `<label>` with `for` on the switch element, or `aria-label`/`aria-labelledby`
101
+ - State text ("ON"/"OFF") inside the track is a visual enhancement only — it MUST NOT be the sole accessible name
102
+ - Disabled: use `aria-disabled="true"` (not native `disabled` attr) if keyboard focus should remain (e.g. to explain why it's disabled via tooltip)
103
+ - Announce state change via `aria-live="polite"` if switching triggers a significant UI change distant from the control
104
+
105
+ ---
106
+
107
+ ## Motion
108
+
109
+ | Transition | Duration | Easing | Notes |
110
+ |------------|----------|--------|-------|
111
+ | Thumb slide | 150ms | spring (bounce: 0) | Smooth, satisfying; canonical spring values |
112
+ | Track colour | 150ms | ease | Simultaneous with thumb |
113
+ | Thumb scale on hover | 80ms | ease | 1→1.1× scale |
114
+ | Press scale | 80ms | ease | 1→0.96× (canonical press scale) |
115
+
116
+ Cross-link: `reference/motion.md` — spring bounce=0 canonical values, canonical scale-on-press 0.96
117
+
118
+ ---
119
+
120
+ ## Do / Don't
121
+
122
+ ### Do
123
+ - Use `role="switch"` not `role="checkbox"` for toggle-with-immediate-effect *(WAI-ARIA APG, Radix)*
124
+ - Animate the thumb sliding — static snap removes the "toggle" affordance *(Apple HIG, Material 3)*
125
+ - Place label to the left of the switch in LTR layouts *(Apple HIG, Material 3, Polaris)*
126
+ - Apply changes immediately on toggle — no submit button required *(Apple HIG, Polaris)*
127
+
128
+ ### Don't
129
+ - Don't use a switch inside a form where the user must click "Save" — use a checkbox *(Apple HIG, Polaris)*
130
+ - Don't rely on track colour alone to communicate state (colour blind users) — add icon or label *(Spectrum, Material 3)*
131
+ - Don't use the same `aria-label` for ON and OFF states — screen readers read the current state via `aria-checked` *(WAI-ARIA APG)*
132
+ - Don't animate thumb with `transition: all` — it catches border-radius changes and causes thumb deformation *(BAN-04)*
133
+
134
+ ---
135
+
136
+ ## Anti-patterns Cross-links
137
+
138
+ | Anti-pattern | Entry |
139
+ |--------------|-------|
140
+ | BAN-04 | `transition: all` — `reference/anti-patterns.md#ban-04` |
141
+ | Checkbox semantics for immediate-action toggle | `reference/anti-patterns.md` |
142
+
143
+ ---
144
+
145
+ ## Benchmark Citations
146
+
147
+ | Claim | Sources |
148
+ |-------|---------|
149
+ | Switch = immediate action; checkbox = form submit | Apple HIG, Material 3, Polaris |
150
+ | role="switch" not role="checkbox" | WAI-ARIA APG, Radix |
151
+ | Pill track (border-radius: 9999px) | Apple HIG, Material 3, Fluent 2 (all 8) |
152
+ | Spring motion for thumb | Material 3, Apple HIG |
153
+
154
+ Full system URLs: `connections/design-corpora.md`
155
+
156
+ ---
157
+
158
+ ## Grep Signatures
159
+
160
+ ```bash
161
+ # Switch using role="checkbox" instead of role="switch"
162
+ grep -rn 'role="checkbox"' src/ | grep -i 'switch\|toggle'
163
+
164
+ # Missing aria-checked on switch
165
+ grep -rn 'role="switch"' src/ | grep -v 'aria-checked'
166
+
167
+ # transition: all on switch thumb (BAN-04)
168
+ grep -rn 'transition:\s*all' src/ | grep -i 'switch\|thumb\|toggle'
169
+
170
+ # Switch inside <form> with submit (should be checkbox)
171
+ grep -rn 'role="switch"\|type.*switch' src/ | grep -i 'form\|submit'
172
+ ```
173
+
174
+ ---
175
+
176
+ ## Failing Example
177
+
178
+ ```html
179
+ <!-- BAD: input[type="checkbox"] used as a switch — wrong semantic contract -->
180
+ <label>
181
+ <input type="checkbox" class="switch-toggle" />
182
+ Enable notifications
183
+ </label>
184
+ ```
185
+
186
+ **Why it fails**: `type="checkbox"` implies form-submit semantics. Screen reader announces "checkbox" not "switch". Users with mental model of "toggle = immediate effect" are confused.
187
+ **Grep detection**: `grep -rn 'class.*switch\|class.*toggle' src/ | grep 'type="checkbox"'`
188
+ **Fix**:
189
+ ```html
190
+ <button role="switch" aria-checked="false" id="notif-switch">
191
+ <span class="thumb" aria-hidden="true"></span>
192
+ </button>
193
+ <label for="notif-switch">Enable notifications</label>
194
+ ```
@@ -0,0 +1,213 @@
1
+ # Tabs — Benchmark Spec
2
+
3
+ **Harvested from**: WAI-ARIA APG, Radix UI, Carbon, Mantine, Material 3, Chakra UI, Atlassian, Fluent 2
4
+ **Wave**: 2 · **Category**: Containers
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+
10
+ Tabs organize content into parallel sections where only one section is visible at a time. Users navigate between tab panels without a page reload. Tabs differ from accordion (tabs are horizontal, panels mutually exclusive) and navigation (tabs do not change the URL in most implementations). The tab strip uses arrow-key navigation within the tablist, not Tab key. *(WAI-ARIA APG, Radix, Carbon all define this contract)*
11
+
12
+ ---
13
+
14
+ ## Anatomy
15
+
16
+ ```
17
+ ┌──────┬──────────┬──────┐
18
+ │ Tab1 │ Tab 2 │ Tab3 │ ← role="tablist"
19
+ └──────┴──────────┴──────┘ Each tab: role="tab" + aria-selected + aria-controls
20
+ ──────────────────────────── Panel separator (visual only)
21
+ Panel content ← role="tabpanel" + aria-labelledby
22
+
23
+ Vertical tabs (sidebar):
24
+ ┌──────────┬──────────────────┐
25
+ │ Tab 1 │ │
26
+ │ Tab 2 │ Panel content │
27
+ │ Tab 3 │ │
28
+ └──────────┴──────────────────┘
29
+ ```
30
+
31
+ | Part | Required | Notes |
32
+ |------|----------|-------|
33
+ | `role="tablist"` container | Yes | Wraps all tabs; `aria-label` or `aria-labelledby` |
34
+ | Tab triggers | Yes | `role="tab"`, `aria-selected`, `aria-controls` |
35
+ | Tab panels | Yes | `role="tabpanel"`, `aria-labelledby` (matching tab id) |
36
+ | Tab strip indicator | No | Underline or filled; shows selected tab |
37
+ | Overflow handling | Conditional | Scroll or dropdown when tabs overflow container |
38
+
39
+ ---
40
+
41
+ ## Variants
42
+
43
+ | Variant | Description | Systems |
44
+ |---------|-------------|---------|
45
+ | Default (horizontal) | Tab strip above panel | All |
46
+ | Vertical | Tab strip left of panel | Carbon, Material 3, Fluent |
47
+ | Underline | Underline indicator below selected tab | Material 3, shadcn, Atlassian |
48
+ | Filled / boxed | Selected tab has filled background | Carbon, Mantine, Fluent |
49
+ | Pill | Rounded tab shape | Mantine, Chakra |
50
+ | Scrollable | Horizontal scroll when tabs overflow | Material 3, Carbon |
51
+ | Icon + label | Icon above or beside label | Material 3, Carbon |
52
+
53
+ **Norm** (≥6/18): arrow keys navigate between tabs; Tab key moves to active panel content.
54
+ **Diverge**: automatic vs. manual activation — automatic (arrow key selects immediately) vs. manual (arrow key moves focus, Enter selects). WAI-ARIA APG recommends manual for complex panels; Radix defaults to automatic.
55
+
56
+ ---
57
+
58
+ ## States
59
+
60
+ | State | ARIA |
61
+ |-------|------|
62
+ | Selected tab | `aria-selected="true"`, `tabindex="0"` |
63
+ | Unselected tab | `aria-selected="false"`, `tabindex="-1"` |
64
+ | Focused tab | 2px focus ring; `tabindex="0"` moves to tab |
65
+ | Disabled tab | `aria-disabled="true"`, `tabindex="-1"` |
66
+ | Active panel | `role="tabpanel"`, visible, focusable |
67
+ | Inactive panel | `hidden` or `display:none` (removed from AT) |
68
+
69
+ ---
70
+
71
+ ## Sizing & Spacing
72
+
73
+ | Property | Value | Notes |
74
+ |----------|-------|-------|
75
+ | Tab height | 40–48px | Touch target compliance |
76
+ | Tab padding H | 16px | Minimum; increase for wider labels |
77
+ | Min tab width | 80px | Prevent cramped labels |
78
+ | Indicator thickness | 2–3px (underline) | On bottom edge of selected tab |
79
+ | Panel padding | 16–24px | |
80
+
81
+ ---
82
+
83
+ ## Typography
84
+
85
+ - Tab label: 14px/500 (selected), 14px/400 (unselected)
86
+ - Vertical tab label: 14px/400; left-aligned
87
+ - Tab count badge: 12px/600 in a pill; `aria-label` on tab includes count
88
+
89
+ ---
90
+
91
+ ## Keyboard & Accessibility
92
+
93
+ > **WAI-ARIA role**: `tablist` (container), `tab` (each trigger), `tabpanel` (each content panel)
94
+ > **Tab attributes**: `aria-selected`, `aria-controls` (panel id), `tabindex` (0 if selected, -1 if not)
95
+ > **Panel attributes**: `role="tabpanel"`, `aria-labelledby` (tab id), `tabindex="0"` (makes panel focusable)
96
+
97
+ ### Keyboard Contract
98
+
99
+ *Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/tabs/ — W3C — 2024*
100
+
101
+ | Key | Action |
102
+ |-----|--------|
103
+ | Tab | When focus moves into the tab list, sets focus on the active tab. When the tab list has focus, Tab moves focus to the next element in the page tab sequence (the tabpanel or element after tablist). |
104
+ | Arrow Right | Moves focus to the next tab. If focus is on the last tab, moves to the first tab. |
105
+ | Arrow Left | Moves focus to the previous tab. If focus is on the first tab, moves to the last tab. |
106
+ | Arrow Down | (Vertical tabs) Moves focus to the next tab |
107
+ | Arrow Up | (Vertical tabs) Moves focus to the previous tab |
108
+ | Space / Enter | (Manual activation only) Activates the focused tab |
109
+ | Home | Moves focus to the first tab |
110
+ | End | Moves focus to the last tab |
111
+
112
+ ### Activation Modes
113
+
114
+ - **Automatic**: arrow key moves focus AND activates the tab/panel simultaneously
115
+ - **Manual**: arrow key moves focus only; Enter/Space activates. Preferred for panels that have expensive load operations
116
+
117
+ ### Accessibility Rules
118
+
119
+ - Only the selected tab has `tabindex="0"` — all other tabs have `tabindex="-1"` (roving tabindex pattern)
120
+ - `tablist` MUST have a label: `aria-label="[Section name]"` or `aria-labelledby`
121
+ - Inactive panels MUST be hidden with `hidden` attribute (not just CSS) so AT skips them
122
+ - Panel SHOULD have `tabindex="0"` to allow focusing the panel after Tab from the tablist
123
+ - Icon-only tabs MUST have `aria-label` on the tab element
124
+ - Linked tabs (tabs that change URL): use `role="link"` semantics or native `<a>` within tab — but note this changes the keyboard contract
125
+
126
+ ---
127
+
128
+ ## Motion
129
+
130
+ | Transition | Duration | Easing | Notes |
131
+ |------------|----------|--------|-------|
132
+ | Indicator slide | 200ms | ease-out | Underline slides between tabs |
133
+ | Panel fade | 150ms | ease | Crossfade between panels |
134
+ | Scroll reveal | 200ms | ease | When scrolling to new active tab in overflow |
135
+
136
+ Cross-link: `reference/motion.md` — `prefers-reduced-motion`: disable indicator slide + panel fade
137
+
138
+ ---
139
+
140
+ ## Do / Don't
141
+
142
+ ### Do
143
+ - Use roving tabindex — `tabindex="0"` on selected, `tabindex="-1"` on all others *(WAI-ARIA APG)*
144
+ - Navigate with arrow keys between tabs, not Tab key *(WAI-ARIA APG)*
145
+ - Label the tablist with `aria-label` or `aria-labelledby` *(WAI-ARIA APG)*
146
+ - Hide inactive panels with `hidden` attribute so AT skips them *(WAI-ARIA APG)*
147
+
148
+ ### Don't
149
+ - Don't use Tab key to navigate between tabs — Tab moves in/out of the tablist *(WAI-ARIA APG)*
150
+ - Don't show all tab panel content simultaneously — defeats the purpose of tabs *(all systems)*
151
+ - Don't use more than 7 tabs in a horizontal tab strip — prefer a select or dropdown for overflow *(Carbon, Atlassian)*
152
+ - Don't use tabs for steps that must be completed in order — use a stepper *(Material 3, Atlassian)*
153
+
154
+ ---
155
+
156
+ ## Anti-patterns Cross-links
157
+
158
+ | Anti-pattern | Entry |
159
+ |--------------|-------|
160
+ | Tab navigation with Tab key (not arrow keys) | `reference/anti-patterns.md` |
161
+ | All panels visible simultaneously | `reference/anti-patterns.md` |
162
+
163
+ ---
164
+
165
+ ## Benchmark Citations
166
+
167
+ | Claim | Sources |
168
+ |-------|---------|
169
+ | Arrow keys navigate tablist | WAI-ARIA APG §3.2 |
170
+ | Tab moves in/out of tablist | WAI-ARIA APG §3.2 |
171
+ | Roving tabindex pattern | WAI-ARIA APG |
172
+ | hidden attr on inactive panels | WAI-ARIA APG |
173
+ | aria-selected="true" on active tab | WAI-ARIA APG, all systems |
174
+
175
+ Full system URLs: `connections/design-corpora.md`
176
+
177
+ ---
178
+
179
+ ## Grep Signatures
180
+
181
+ ```bash
182
+ # Tabs using Tab key navigation instead of arrow keys
183
+ grep -rn 'role="tab"' src/ | xargs grep -l 'onKeyDown.*Tab\|key.*Tab' 2>/dev/null | grep -v 'ArrowLeft\|ArrowRight'
184
+
185
+ # Tab missing aria-selected
186
+ grep -rn 'role="tab"' src/ | grep -v 'aria-selected'
187
+
188
+ # Inactive panel not hidden (just CSS)
189
+ grep -rn 'role="tabpanel"' src/ | grep -v 'hidden\|aria-hidden'
190
+
191
+ # tablist missing accessible label
192
+ grep -rn 'role="tablist"' src/ | grep -v 'aria-label\|aria-labelledby'
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Failing Example
198
+
199
+ ```html
200
+ <!-- BAD: tabs using Tab key for navigation + no aria attributes -->
201
+ <div class="tab-list">
202
+ <button class="tab active">Overview</button>
203
+ <button class="tab">Details</button>
204
+ <button class="tab">Reviews</button>
205
+ </div>
206
+ <div class="tab-panel active">Overview content</div>
207
+ <div class="tab-panel">Details content</div>
208
+ <div class="tab-panel">Reviews content</div>
209
+ ```
210
+
211
+ **Why it fails**: No `role="tablist"`, `role="tab"`, `role="tabpanel"`. No `aria-selected`. No `aria-controls`/`aria-labelledby`. Tab key moves between buttons instead of arrow keys. Inactive panels are not hidden from AT.
212
+ **Grep detection**: `grep -rn 'class.*tab\b' src/ | grep -v 'role='`
213
+ **Fix**: Use Radix `<Tabs>` or implement WAI-ARIA tabs pattern with `role="tablist"`, `role="tab"` (with `aria-selected`, `aria-controls`, roving tabindex), `role="tabpanel"` (with `aria-labelledby`), and arrow-key handlers.