@hegemonart/get-design-done 1.15.0 → 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.
- package/.claude-plugin/marketplace.json +5 -3
- package/.claude-plugin/plugin.json +4 -2
- package/CHANGELOG.md +38 -0
- package/README.md +23 -0
- package/SKILL.md +4 -1
- package/agents/component-benchmark-harvester.md +112 -0
- package/agents/component-benchmark-synthesizer.md +88 -0
- package/connections/design-corpora.md +158 -0
- package/package.json +4 -2
- package/reference/components/README.md +90 -0
- package/reference/components/TEMPLATE.md +184 -0
- package/reference/components/accordion.md +217 -0
- package/reference/components/button.md +195 -0
- package/reference/components/card.md +200 -0
- package/reference/components/checkbox.md +207 -0
- package/reference/components/drawer.md +201 -0
- package/reference/components/input.md +208 -0
- package/reference/components/label.md +200 -0
- package/reference/components/link.md +193 -0
- package/reference/components/modal-dialog.md +210 -0
- package/reference/components/popover.md +197 -0
- package/reference/components/radio.md +203 -0
- package/reference/components/select-combobox.md +219 -0
- package/reference/components/switch.md +194 -0
- package/reference/components/tabs.md +213 -0
- package/reference/components/tooltip.md +201 -0
- package/reference/registry.json +102 -0
- package/reference/registry.schema.json +2 -1
- 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.
|