@hegemonart/get-design-done 1.15.0 → 1.18.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 (70) hide show
  1. package/.claude-plugin/marketplace.json +9 -5
  2. package/.claude-plugin/plugin.json +19 -5
  3. package/CHANGELOG.md +122 -0
  4. package/README.md +41 -0
  5. package/SKILL.md +4 -1
  6. package/agents/component-benchmark-harvester.md +112 -0
  7. package/agents/component-benchmark-synthesizer.md +88 -0
  8. package/agents/design-auditor.md +60 -1
  9. package/agents/design-doc-writer.md +21 -0
  10. package/agents/design-executor.md +22 -4
  11. package/agents/design-pattern-mapper.md +61 -0
  12. package/agents/motion-mapper.md +74 -9
  13. package/agents/token-mapper.md +8 -0
  14. package/connections/design-corpora.md +158 -0
  15. package/package.json +13 -3
  16. package/reference/components/README.md +94 -0
  17. package/reference/components/TEMPLATE.md +184 -0
  18. package/reference/components/accordion.md +217 -0
  19. package/reference/components/alert.md +198 -0
  20. package/reference/components/badge.md +202 -0
  21. package/reference/components/breadcrumbs.md +198 -0
  22. package/reference/components/button.md +195 -0
  23. package/reference/components/card.md +200 -0
  24. package/reference/components/checkbox.md +207 -0
  25. package/reference/components/chip.md +209 -0
  26. package/reference/components/command-palette.md +228 -0
  27. package/reference/components/date-picker.md +227 -0
  28. package/reference/components/drawer.md +201 -0
  29. package/reference/components/file-upload.md +219 -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/list.md +217 -0
  34. package/reference/components/menu.md +212 -0
  35. package/reference/components/modal-dialog.md +210 -0
  36. package/reference/components/navbar.md +211 -0
  37. package/reference/components/pagination.md +205 -0
  38. package/reference/components/popover.md +197 -0
  39. package/reference/components/progress.md +210 -0
  40. package/reference/components/radio.md +203 -0
  41. package/reference/components/rich-text-editor.md +226 -0
  42. package/reference/components/select-combobox.md +219 -0
  43. package/reference/components/sidebar.md +211 -0
  44. package/reference/components/skeleton.md +197 -0
  45. package/reference/components/slider.md +208 -0
  46. package/reference/components/stepper.md +220 -0
  47. package/reference/components/switch.md +194 -0
  48. package/reference/components/table.md +229 -0
  49. package/reference/components/tabs.md +213 -0
  50. package/reference/components/toast.md +200 -0
  51. package/reference/components/tooltip.md +201 -0
  52. package/reference/components/tree.md +225 -0
  53. package/reference/css-grid-layout.md +835 -0
  54. package/reference/external/NOTICE.hyperframes +28 -0
  55. package/reference/image-optimization.md +582 -0
  56. package/reference/motion-advanced.md +754 -0
  57. package/reference/motion-easings.md +381 -0
  58. package/reference/motion-interpolate.md +282 -0
  59. package/reference/motion-spring.md +234 -0
  60. package/reference/motion-transition-taxonomy.md +155 -0
  61. package/reference/motion.md +20 -0
  62. package/reference/output-contracts/motion-map.schema.json +135 -0
  63. package/reference/registry.json +285 -0
  64. package/reference/registry.schema.json +6 -1
  65. package/reference/variable-fonts-loading.md +532 -0
  66. package/scripts/lib/easings.cjs +280 -0
  67. package/scripts/lib/parse-contract.cjs +220 -0
  68. package/scripts/lib/spring.cjs +160 -0
  69. package/scripts/tests/test-motion-provenance.sh +64 -0
  70. package/skills/benchmark/SKILL.md +105 -0
@@ -0,0 +1,193 @@
1
+ # Link — Benchmark Spec
2
+
3
+ **Harvested from**: Carbon, Polaris, Primer (GitHub), Fluent 2, WAI-ARIA APG, Material 3, Mantine, Atlassian
4
+ **Wave**: 1 · **Category**: Inputs
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+
10
+ A link navigates the user to a new resource — another page, section, or external URL. It is the semantic counterpart to Button: if clicking takes the user somewhere, it is a `<a href>` link; if it triggers an action in the current context, it is a button. Never reverse these roles. *(Carbon, Primer, WAI-ARIA APG all enforce this boundary)*
11
+
12
+ ---
13
+
14
+ ## Anatomy
15
+
16
+ ```
17
+ Inline: Read the [full documentation] for details.
18
+ └── <a href="..."> — inline, within body text
19
+
20
+ Standalone: [→ View report]
21
+ └── <a href="..."> — block-level, not inline in prose
22
+
23
+ External: [Open dashboard ↗]
24
+ └── <a href="..." target="_blank" rel="noopener noreferrer">
25
+ + aria-label="Open dashboard (opens in new tab)"
26
+ ```
27
+
28
+ | Part | Required | Notes |
29
+ |------|----------|-------|
30
+ | `<a href>` | Yes | Native element; href MUST be a real URL or `#anchor` |
31
+ | Visible label | Yes | Descriptive of destination — not "click here" or "read more" |
32
+ | Underline | Conditional | Required for inline links in body text; optional for standalone/nav |
33
+ | Visited state | No | Encouraged for inline links in long-form content |
34
+ | External icon | Conditional | Required when `target="_blank"`; 12–14px, inline-aligned |
35
+ | `rel="noopener noreferrer"` | Yes (external) | Security — prevents opener access |
36
+
37
+ ---
38
+
39
+ ## Variants
40
+
41
+ | Variant | Description | Systems |
42
+ |---------|-------------|---------|
43
+ | Inline | Within prose; always underlined | All |
44
+ | Standalone | Block-level CTA; underline optional | Carbon, Polaris, Primer |
45
+ | Nav / breadcrumb | In navigation contexts; no underline | All |
46
+ | External | `target="_blank"` with external icon | All |
47
+ | Destructive | Rare; red colour for delete-via-link patterns | Polaris (critical link) |
48
+ | Disabled | `aria-disabled="true"` + `tabindex="-1"` | Carbon, Primer |
49
+
50
+ **Norm** (≥6/18): inline links in body text MUST be underlined — colour alone fails WCAG 1.4.1.
51
+ **Diverge**: visited state — Primer and Carbon use it for documentation; most SaaS systems omit it.
52
+
53
+ ---
54
+
55
+ ## States
56
+
57
+ | State | Visual | ARIA / HTML |
58
+ |-------|--------|-------------|
59
+ | default | Underline + colour | `href` present |
60
+ | hover | Colour shift (10% darker/lighter) | — |
61
+ | focus | 2px focus-visible ring | — |
62
+ | active / pressed | Colour darkens + subtle scale 0.98 | — |
63
+ | visited | Distinct colour (purple conventional) | `:visited` pseudo-class |
64
+ | disabled | 38% opacity; pointer-events: none | `aria-disabled="true"` + `tabindex="-1"` |
65
+
66
+ ---
67
+
68
+ ## Sizing & Spacing
69
+
70
+ - Links inherit parent font-size and line-height — do not override
71
+ - Standalone link min-height: 44px via padding for touch targets
72
+ - Icon size (external/leading): 12–14px; `vertical-align: middle`; 4px gap from text
73
+
74
+ Cross-link: `reference/surfaces.md` — hit-area pattern for standalone links
75
+
76
+ ---
77
+
78
+ ## Typography
79
+
80
+ - Inline links: same weight as surrounding text (400); underline distinguishes them
81
+ - Standalone links: 400–500 weight; may have leading icon or trailing arrow
82
+ - Never use ALL CAPS for links — reduces readability and implies different semantics
83
+ - Truncate with ellipsis + `title` attribute only when space is genuinely constrained
84
+
85
+ ---
86
+
87
+ ## Keyboard & Accessibility
88
+
89
+ > **WAI-ARIA role**: `link` (implicit on `<a href>`)
90
+ > **Required attributes**: `href` — without it, the element is not a link and has no keyboard access
91
+
92
+ ### Keyboard Contract
93
+
94
+ *Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/link/ — W3C — 2024*
95
+
96
+ | Key | Action |
97
+ |-----|--------|
98
+ | Enter | Activates the link and navigates to its destination |
99
+ | Tab | Moves focus to the next focusable element |
100
+ | Shift+Tab | Moves focus to the previous focusable element |
101
+
102
+ ### Accessibility Rules
103
+
104
+ - Link text MUST describe the destination — "click here" and "read more" fail 2.4.6 (descriptive labels)
105
+ - External links opening in new tab MUST disclose this: append "(opens in new tab)" to `aria-label`, or use a visually-hidden span
106
+ - `target="_blank"` MUST always be paired with `rel="noopener noreferrer"` (security + performance)
107
+ - Disabled links: `aria-disabled="true"` + `tabindex="-1"` — never `href=""` or `href="#"`
108
+ - Inline links in body text MUST be underlined — colour alone is insufficient for WCAG 1.4.1 (non-text contrast)
109
+ - Icon-only links (e.g., social icons) MUST have `aria-label` describing the destination
110
+
111
+ ---
112
+
113
+ ## Motion
114
+
115
+ | Transition | Duration | Easing | Notes |
116
+ |------------|----------|--------|-------|
117
+ | Colour on hover | 100ms | ease | Subtle; avoid opacity changes (readability) |
118
+ | Underline decoration | 80ms | ease | Underline grow or fade for standalone variants |
119
+
120
+ ---
121
+
122
+ ## Do / Don't
123
+
124
+ ### Do
125
+ - Use descriptive link text: "View account settings" not "click here" *(Polaris, Carbon, WAI-ARIA APG)*
126
+ - Underline inline links in body text *(Carbon, Polaris, WAI-ARIA APG — WCAG 1.4.1)*
127
+ - Add `rel="noopener noreferrer"` to all `target="_blank"` links *(Primer, Carbon, Fluent 2)*
128
+ - Disclose new-tab behavior in `aria-label` or visually-hidden text *(WAI-ARIA APG, Primer)*
129
+
130
+ ### Don't
131
+ - Don't use `<a>` without `href` — it's not a link, not keyboard-accessible, and will confuse screen readers *(WAI-ARIA APG)*
132
+ - Don't use `<button>` when the action is navigation *(Carbon, Primer)*
133
+ - Don't rely on colour alone to distinguish links from surrounding text *(WCAG 1.4.1)*
134
+ - Don't open links in new tabs unexpectedly without disclosure *(Polaris, Primer, WCAG 3.2.5)*
135
+
136
+ ---
137
+
138
+ ## Anti-patterns Cross-links
139
+
140
+ | Anti-pattern | Entry |
141
+ |--------------|-------|
142
+ | Anchor without href | `reference/anti-patterns.md` |
143
+ | Non-descriptive link text | `reference/anti-patterns.md` |
144
+
145
+ ---
146
+
147
+ ## Benchmark Citations
148
+
149
+ | Claim | Sources |
150
+ |-------|---------|
151
+ | Enter activates link | WAI-ARIA APG §3.3 |
152
+ | Underline required for inline links | Carbon, Polaris, WCAG 1.4.1 |
153
+ | rel="noopener noreferrer" for _blank | Primer, Carbon, Fluent 2 |
154
+ | "click here" is anti-pattern | Polaris, Carbon, WAI-ARIA APG |
155
+
156
+ Full system URLs: `connections/design-corpora.md`
157
+
158
+ ---
159
+
160
+ ## Grep Signatures
161
+
162
+ ```bash
163
+ # <a> without href (not a link — no keyboard access)
164
+ grep -rn '<a ' src/ | grep -v 'href='
165
+
166
+ # target="_blank" without rel="noopener noreferrer"
167
+ grep -rn 'target="_blank"' src/ | grep -v 'rel=.*noopener'
168
+
169
+ # Non-descriptive link text
170
+ grep -rn '>click here\|>read more\|>learn more\|>here<' src/ | grep -i '<a'
171
+
172
+ # Colour-only link distinction (no text-decoration)
173
+ grep -rn 'text-decoration:\s*none' src/ | grep -i 'link\|<a'
174
+ ```
175
+
176
+ ---
177
+
178
+ ## Failing Example
179
+
180
+ ```html
181
+ <!-- BAD: non-descriptive link text + missing rel on external link -->
182
+ <a href="https://docs.example.com" target="_blank">Click here</a>
183
+ ```
184
+
185
+ **Why it fails**: "Click here" gives no destination context (fails WCAG 2.4.6). Missing `rel="noopener noreferrer"` is a security vulnerability. No disclosure of new-tab behavior.
186
+ **Grep detection**: `grep -rn '>click here\|>here<\|>read more' src/`
187
+ **Fix**:
188
+ ```html
189
+ <a href="https://docs.example.com" target="_blank" rel="noopener noreferrer"
190
+ aria-label="View documentation (opens in new tab)">
191
+ View documentation <span aria-hidden="true">↗</span>
192
+ </a>
193
+ ```
@@ -0,0 +1,217 @@
1
+ # List (Interactive & Display) — Benchmark Spec
2
+
3
+ **Harvested from**: Carbon, Polaris, Material 3, Mantine, WAI-ARIA APG, UUPM (app-interface, MIT)
4
+ **Wave**: 4 · **Category**: Navigation & Data
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+
10
+ A list component handles two distinct patterns: (1) a display list renders a series of items using semantic `<ul>/<ol>/<li>` HTML — no ARIA needed; (2) an interactive list (listbox) presents selectable options with keyboard navigation and selection state. Use a display list for content; use an interactive listbox when users choose one or more items from a set. *(Carbon, Polaris, Material 3 all define separate display and interactive list patterns)*
11
+
12
+ ---
13
+
14
+ ## Anatomy
15
+
16
+ ```
17
+ Display list: Interactive listbox:
18
+ <ul> <div role="listbox"
19
+ <li>Item one</li> aria-multiselectable="false"
20
+ <li>Item two</li> aria-label="Assignees">
21
+ <li>Item three</li> <div role="option"
22
+ </ul> aria-selected="true">Alice</div>
23
+ <div role="option"
24
+ aria-selected="false">Bob</div>
25
+ </div>
26
+ ```
27
+
28
+ | Part | Required | Notes |
29
+ |------|----------|-------|
30
+ | List container | Yes | `<ul>` / `<ol>` (display) or `role="listbox"` (interactive) |
31
+ | List item | Yes | `<li>` (display) or `role="option"` (interactive) |
32
+ | `aria-selected` | Interactive only | `true`/`false` on each `role="option"` |
33
+ | `aria-multiselectable` | Interactive only | `true` if multi-select; default `false` |
34
+ | `aria-label` / `aria-labelledby` | Interactive only | Describes the listbox purpose |
35
+ | Empty state | No | Min 200px height; illustration + message + optional CTA |
36
+ | Virtual scroll | Conditional | At > 100 items; TanStack Virtual or react-virtual |
37
+
38
+ ---
39
+
40
+ ## Variants
41
+
42
+ | Variant | Description | Systems |
43
+ |---------|-------------|---------|
44
+ | Unordered display | `<ul>` bullet list; purely semantic | All systems |
45
+ | Ordered display | `<ol>` numbered list; sequential content | All systems |
46
+ | Single-select listbox | One item selectable at a time | Carbon, Polaris, Material 3 |
47
+ | Multi-select listbox | Multiple items selectable (Shift+Click, Ctrl+Click) | Carbon, Material 3, Mantine |
48
+ | List / detail panel | Left-panel list + right detail pane | UUPM app-interface (MIT) |
49
+ | Recent-items list | Time-ordered recent items with timestamps | UUPM app-interface (MIT) |
50
+ | Virtualized | Windowed rendering for large datasets (> 100 items) | Carbon, Mantine |
51
+
52
+ **Norm** (≥4 systems agree): `role="listbox"` + `role="option"` for interactive; native `<ul>/<li>` for display.
53
+ **Diverge**: Material 3 calls the interactive variant "ListItem with selectable state"; Carbon uses "ContentSwitcher" for small sets and "MultiSelect" for large. Pattern semantics are identical.
54
+
55
+ ---
56
+
57
+ ## States
58
+
59
+ | State | Trigger | Visual | ARIA |
60
+ |-------|---------|--------|------|
61
+ | default | — | Items visible; none selected | `aria-selected="false"` on all options |
62
+ | option-hover | pointer over | 8% overlay | — |
63
+ | option-focus | keyboard focus | 2px focus-visible ring | managed via `tabindex` |
64
+ | option-selected | click or Enter/Space | Filled highlight; checkmark for multi-select | `aria-selected="true"` |
65
+ | option-disabled | disabled prop | 38% opacity; cursor: default | `aria-disabled="true"` |
66
+ | empty | no items | Empty state (illustration + text + CTA) | `aria-label` on empty container |
67
+ | loading | data fetch | Skeleton items | `aria-busy="true"` on listbox container |
68
+
69
+ ---
70
+
71
+ ## Sizing & Spacing
72
+
73
+ | Element | Value | Notes |
74
+ |---------|-------|-------|
75
+ | Item height | 40px (default) | 32px compact; 48px comfortable |
76
+ | Item padding H | 12–16px | Icon + 8px gap if icon present |
77
+ | Empty state min-height | 200px | Prevents visually collapsed empty container |
78
+ | Virtual viewport | ~400–600px | Clip height for virtualised scroll |
79
+ | List max-height | 400px default | Scroll within list container |
80
+
81
+ **Norm**: 40px item height (Carbon, Polaris, Mantine). Max-height + internal scroll for contained lists.
82
+
83
+ ---
84
+
85
+ ## Typography
86
+
87
+ - Display list: inherits parent body text; `<li>` marker via `list-style-type`
88
+ - Interactive option label: body-sm (13–14px), weight 400; selected weight 500
89
+ - Secondary text / metadata: label-xs (11–12px), `color: --text-subtle`
90
+ - Empty state heading: heading-sm, center-aligned
91
+ - Empty state body: body-sm, `color: --text-subtle`
92
+
93
+ Cross-link: `reference/typography.md` — body-sm, heading scale
94
+
95
+ ---
96
+
97
+ ## Keyboard & Accessibility
98
+
99
+ > **WAI-ARIA role**: `listbox` (interactive container), `option` (each item)
100
+ > **Required attributes**: `aria-selected` on each `role="option"`; `aria-label` or `aria-labelledby` on `role="listbox"`; `aria-multiselectable="true"` for multi-select
101
+
102
+ ### Keyboard Contract
103
+
104
+ *Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/listbox/ — W3C — 2024*
105
+
106
+ | Key | Action |
107
+ |-----|--------|
108
+ | ArrowDown | Moves focus to next option (wraps to first) |
109
+ | ArrowUp | Moves focus to previous option (wraps to last) |
110
+ | Home | Moves focus to first option |
111
+ | End | Moves focus to last option |
112
+ | Enter / Space | Selects the focused option (single-select) |
113
+ | Shift+ArrowDown | Extends selection downward (multi-select) |
114
+ | Shift+ArrowUp | Extends selection upward (multi-select) |
115
+ | Ctrl+A | Selects all options (multi-select) |
116
+ | A–Z | Moves focus to next option starting with typed character |
117
+
118
+ ### Accessibility Rules
119
+
120
+ - Display lists use native `<ul>/<li>` — no ARIA roles needed; they are already accessible
121
+ - Interactive lists MUST use `role="listbox"` + `role="option"` — not `<ul>/<li>` with click handlers
122
+ - `aria-selected` MUST be present on every `role="option"` (either `true` or `false`)
123
+ - Multi-select listbox MUST declare `aria-multiselectable="true"` on the container
124
+ - Virtual scroll: all options in the virtualised window must have correct `aria-posinset` and `aria-setsize` attributes
125
+ - Empty state container MUST have `aria-label` or `aria-live` so AT announces the empty state
126
+
127
+ Cross-link: `reference/accessibility.md` — listbox pattern, virtual list accessibility
128
+
129
+ ---
130
+
131
+ ## Motion
132
+
133
+ | Transition | Duration | Easing | Notes |
134
+ |------------|----------|--------|-------|
135
+ | Option selection highlight | 100ms | ease-out | Background color only |
136
+ | Skeleton item shimmer | 1500ms | linear loop | Loading placeholder |
137
+ | Empty state entry | 150ms | ease-out | Fade in |
138
+
139
+ **BAN**: Do not animate item reordering unless using a deliberate drag-and-drop library — unsolicited reordering causes disorientation.
140
+
141
+ Cross-link: `reference/motion.md` — skeleton shimmer, list animations
142
+
143
+ ---
144
+
145
+ ## Do / Don't
146
+
147
+ ### Do
148
+ - Use native `<ul>/<ol>/<li>` for display-only lists — no ARIA needed *(WAI-ARIA APG)*
149
+ - Use `role="listbox"` + `role="option"` for selectable lists *(WAI-ARIA APG, Carbon, Polaris)*
150
+ - Virtualise at > 100 items to prevent DOM bloat *(Carbon, Mantine)*
151
+ - Provide a meaningful empty state with a CTA when the list can be populated *(Polaris, Material 3)*
152
+
153
+ ### Don't
154
+ - Don't use `<div onClick>` list items without `role="option"` — keyboard-inaccessible *(WCAG 2.1.1)*
155
+ - Don't omit `aria-selected` on options — AT cannot determine what is selected *(WAI-ARIA APG)*
156
+ - Don't use `<ul>/<li>` with `role="option"` — mixing native list semantics and listbox ARIA creates conflicts *(WAI-ARIA)*
157
+ - Don't load all items at once when count > 100 — renders slowly and wastes memory *(Carbon, Mantine)*
158
+
159
+ ---
160
+
161
+ ## Anti-patterns Cross-links
162
+
163
+ | Anti-pattern | Entry |
164
+ |--------------|-------|
165
+ | BAN-04 | `transition: all` on interactive elements — `reference/anti-patterns.md#ban-04` |
166
+
167
+ ---
168
+
169
+ ## Benchmark Citations
170
+
171
+ | Claim | Sources |
172
+ |-------|---------|
173
+ | role="listbox" + role="option" for interactive lists | WAI-ARIA APG listbox pattern |
174
+ | aria-selected required on every option | WAI-ARIA APG, Carbon, Polaris |
175
+ | aria-multiselectable="true" for multi-select | WAI-ARIA APG |
176
+ | Virtualise at > 100 items | Carbon, Mantine (TanStack Virtual) |
177
+ | 200px min empty state height | Carbon, Polaris HIG |
178
+
179
+ Full system URLs: `connections/design-corpora.md`
180
+
181
+ ---
182
+
183
+ ## Grep Signatures
184
+
185
+ ```bash
186
+ # Interactive list items using <div> without role="option"
187
+ grep -rn '<div' src/ | grep -i 'list.*item\|list-item\|listitem' | grep 'onClick\|on:click' | grep -v 'role='
188
+
189
+ # Missing aria-selected on option elements
190
+ grep -rn 'role="option"' src/ | grep -v 'aria-selected'
191
+
192
+ # Listbox missing aria-label
193
+ grep -rn 'role="listbox"' src/ | grep -v 'aria-label\|aria-labelledby'
194
+
195
+ # <ul>/<li> used with role="option" (semantics conflict)
196
+ grep -rn 'role="option"' src/ | grep '<li'
197
+ ```
198
+
199
+ ---
200
+
201
+ ## Failing Example
202
+
203
+ ```html
204
+ <!-- BAD: interactive list items using <div onClick> with no keyboard support -->
205
+ <div class="user-list"> <!-- no role="listbox" -->
206
+ <div class="list-item selected" onclick="selectUser('alice')">
207
+ Alice Chen
208
+ </div>
209
+ <div class="list-item" onclick="selectUser('bob')">
210
+ Bob Tanaka
211
+ </div>
212
+ </div>
213
+ ```
214
+
215
+ **Why it fails**: No `role="listbox"` on container; no `role="option"` on items; no `aria-selected`; items not keyboard-focusable (no `tabindex`); arrow-key navigation does nothing; screen readers see two unlabelled `<div>` elements.
216
+ **Grep detection**: `grep -rn '<div.*onClick\|<div.*on:click' src/ | grep -i 'list.*item\|listitem'`
217
+ **Fix**: Use `<div role="listbox" aria-label="Users">` with `<div role="option" tabindex="-1" aria-selected="false">` items; implement roving `tabindex` and arrow-key handlers.
@@ -0,0 +1,212 @@
1
+ # Menu (Dropdown / Context Menu) — Benchmark Spec
2
+
3
+ **Harvested from**: Radix UI, WAI-ARIA APG, Carbon, Atlassian, Material 3, Polaris
4
+ **Wave**: 4 · **Category**: Navigation
5
+
6
+ ---
7
+
8
+ ## Purpose
9
+
10
+ A menu presents a list of actions or options in a temporary overlay anchored to a trigger element. It differs from a Select/Combobox (which chooses a value) — a menu executes commands or navigates. Use a Dropdown Menu for trigger-button scenarios; use a Context Menu for right-click/long-press on an element. *(Radix DropdownMenu, Carbon OverflowMenu, Atlassian DropdownMenu, WAI-ARIA APG Menu agree: menus are action lists, not value selectors)*
11
+
12
+ ---
13
+
14
+ ## Anatomy
15
+
16
+ ```
17
+ [ Trigger Button ▾ ]
18
+ ┌─────────────────────┐
19
+ │ ✓ Menu Item │ role="menuitemcheckbox"
20
+ │ ── Separator ── │ role="separator"
21
+ │ › Sub-menu Item │ role="menuitem" aria-haspopup="menu"
22
+ │ Edit │ role="menuitem"
23
+ │ Delete │ role="menuitem"
24
+ └─────────────────────┘
25
+ ```
26
+
27
+ | Part | Required | Notes |
28
+ |------|----------|-------|
29
+ | Trigger | Yes | `<button>` with `aria-haspopup="menu"` + `aria-expanded` |
30
+ | Menu container | Yes | `role="menu"` + `aria-labelledby` pointing to trigger |
31
+ | Menu item | Yes | `role="menuitem"` on each action |
32
+ | Separator | No | `role="separator"` — groups related items |
33
+ | Checkbox item | No | `role="menuitemcheckbox"` + `aria-checked` |
34
+ | Radio item | No | `role="menuitemradio"` + `aria-checked`; group in `role="group"` |
35
+ | Sub-menu trigger | No | `role="menuitem"` + `aria-haspopup="menu"` + `aria-expanded` |
36
+ | Focus indicator | Yes | 2px focus-visible ring on keyboard-active item |
37
+
38
+ ---
39
+
40
+ ## Variants
41
+
42
+ | Variant | Description | Systems |
43
+ |---------|-------------|---------|
44
+ | Dropdown menu | Opens from a button trigger; anchored below/above | Radix, Carbon, Atlassian, Polaris |
45
+ | Context menu | Opens at pointer position on right-click or long-press | Radix, Material 3, Carbon |
46
+ | Overflow menu | Icon-only trigger (⋯ or ⋮); common in dense UIs | Carbon OverflowMenu, Atlassian |
47
+ | Sub-menu | Cascading child menu opening on ArrowRight | Radix, Atlassian, Carbon |
48
+ | Checkbox/radio menu | Items with persistent checked state | Radix, Carbon, Material 3 |
49
+
50
+ **Norm** (≥4 systems agree): flat action list with separator groups; avoid > 2 nesting levels.
51
+ **Diverge**: Polaris uses ActionList as a shared primitive for both menus and select options; Carbon splits OverflowMenu from ContextMenu as separate components.
52
+
53
+ ---
54
+
55
+ ## States
56
+
57
+ | State | Trigger | Visual | ARIA |
58
+ |-------|---------|--------|------|
59
+ | closed | — | Trigger visible; overlay hidden | `aria-expanded="false"` on trigger |
60
+ | open | Click trigger | Overlay visible; first item focused | `aria-expanded="true"` on trigger |
61
+ | item-hover / focus | Arrow keys or pointer | Item highlight (8% overlay) | `tabindex="-1"` managed via roving tabindex |
62
+ | item-disabled | `disabled` prop | 38% opacity, cursor: default | `aria-disabled="true"` on item |
63
+ | checked | Toggle menuitemcheckbox | Checkmark icon visible | `aria-checked="true"` |
64
+ | submenu-open | ArrowRight on parent | Child menu visible | `aria-expanded="true"` on parent item |
65
+
66
+ ---
67
+
68
+ ## Sizing & Spacing
69
+
70
+ | Element | Value | Notes |
71
+ |---------|-------|-------|
72
+ | Min menu width | 160px | Prevents awkward narrow menus |
73
+ | Max menu width | 320px | Truncate labels with ellipsis beyond |
74
+ | Item height | 36px (md) | 32px compact, 40px comfortable |
75
+ | Item padding H | 12px | Icon if present: 16px left + 8px gap |
76
+ | Separator height | 1px + 4px V margin | Divider line |
77
+ | Icon size | 16px | Left-aligned, consistent with label baseline |
78
+
79
+ **Norm**: 36px item height (Radix default, Carbon, Atlassian). Min-width 160px prevents single-word menus.
80
+
81
+ ---
82
+
83
+ ## Typography
84
+
85
+ - Item label: body-sm (13–14px), weight 400 — not bold; action labels read as text, not controls
86
+ - Destructive items: same weight, color token `--color-text-danger`
87
+ - Keyboard shortcut hints: body-xs (11–12px), muted color, right-aligned, `aria-hidden="true"`
88
+ - Truncate item labels with `text-overflow: ellipsis`; never wrap item text
89
+
90
+ Cross-link: `reference/typography.md` — body-sm scale
91
+
92
+ ---
93
+
94
+ ## Keyboard & Accessibility
95
+
96
+ > **WAI-ARIA role**: `menu` (container), `menuitem` / `menuitemcheckbox` / `menuitemradio` (items)
97
+ > **Required attributes**: `aria-haspopup="menu"` + `aria-expanded` on trigger; `aria-labelledby` on `role="menu"`
98
+
99
+ ### Keyboard Contract
100
+
101
+ *Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/ — W3C — 2024*
102
+
103
+ | Key | Action |
104
+ |-----|--------|
105
+ | Enter / Space | Opens menu from trigger; activates focused item |
106
+ | ArrowDown | Moves focus to next item (wraps to first) |
107
+ | ArrowUp | Moves focus to previous item (wraps to last) |
108
+ | Escape | Closes menu; returns focus to trigger |
109
+ | Tab | Closes menu; moves focus to next focusable element (does not cycle through items) |
110
+ | Home | Moves focus to first item |
111
+ | End | Moves focus to last item |
112
+ | A–Z / a–z | Moves focus to next item starting with that character |
113
+ | ArrowRight | Opens sub-menu; moves focus to first item of sub-menu |
114
+ | ArrowLeft | Closes sub-menu; returns focus to parent item |
115
+
116
+ ### Accessibility Rules
117
+
118
+ - Menu MUST open on click only — never on hover for primary open (hover may preview sub-menus)
119
+ - All items MUST be reachable by keyboard; no mouse-only items
120
+ - Focus returns to the trigger element when the menu closes
121
+ - Keyboard shortcut labels (e.g. "⌘K") are `aria-hidden="true"` — the shortcut must be registered separately
122
+ - `role="separator"` dividers are not focusable
123
+ - Disabled items use `aria-disabled="true"` (keep focusable so AT users know the option exists)
124
+
125
+ Cross-link: `reference/accessibility.md` — focus management, roving tabindex
126
+
127
+ ---
128
+
129
+ ## Motion
130
+
131
+ | Transition | Duration | Easing | Notes |
132
+ |------------|----------|--------|-------|
133
+ | Menu enter | 120ms | ease-out | Scale 0.95→1 + opacity 0→1 from anchor point |
134
+ | Menu exit | 80ms | ease-in | Opacity 1→0; skip scale-down for speed |
135
+ | Item highlight | 80ms | ease-out | Background color transition only |
136
+ | Sub-menu enter | 120ms | ease-out | Same as menu enter |
137
+
138
+ **BAN**: `transition: all` on menu items — triggers layout thrash on width changes.
139
+
140
+ Cross-link: `reference/motion.md` — overlay entry pattern, BAN-04
141
+
142
+ ---
143
+
144
+ ## Do / Don't
145
+
146
+ ### Do
147
+ - Use `role="menu"` + `role="menuitem"` for all action menus *(WAI-ARIA APG)*
148
+ - Group related items with `role="separator"` — keep groups ≤ 7 items *(Carbon, Atlassian)*
149
+ - Return focus to the trigger on close *(WAI-ARIA APG)*
150
+ - Use `role="menuitemcheckbox"` for persistent toggle states *(Radix, Material 3)*
151
+
152
+ ### Don't
153
+ - Don't open the menu on hover as the primary interaction — keyboard users can't discover hover *(WCAG 1.3.3)*
154
+ - Don't exceed 2 levels of sub-menus — deeply nested menus are cognitively expensive *(Atlassian, Carbon)*
155
+ - Don't put form controls (inputs, sliders) inside a menu — use a Popover instead *(WAI-ARIA APG)*
156
+ - Don't use `<div>` items without `role="menuitem"` — invisible to screen readers *(WAI-ARIA)*
157
+
158
+ ---
159
+
160
+ ## Anti-patterns Cross-links
161
+
162
+ | Anti-pattern | Entry |
163
+ |--------------|-------|
164
+ | BAN-04 | `transition: all` on interactive elements — `reference/anti-patterns.md#ban-04` |
165
+
166
+ ---
167
+
168
+ ## Benchmark Citations
169
+
170
+ | Claim | Sources |
171
+ |-------|---------|
172
+ | role="menu" + role="menuitem" contract | WAI-ARIA APG Menu Button pattern |
173
+ | Click-only open (not hover) | WAI-ARIA APG, WCAG 1.3.3, Carbon |
174
+ | ArrowRight/Left for sub-menu navigation | WAI-ARIA APG, Radix DropdownMenu |
175
+ | Focus returns to trigger on close | WAI-ARIA APG, Radix |
176
+ | 36px item height | Radix default, Carbon OverflowMenu, Atlassian |
177
+
178
+ Full system URLs: `connections/design-corpora.md`
179
+
180
+ ---
181
+
182
+ ## Grep Signatures
183
+
184
+ ```bash
185
+ # Menu container missing role="menu"
186
+ grep -rn 'dropdown\|context-menu\|overflow-menu' src/ | grep -v 'role="menu"'
187
+
188
+ # Items using <div> without role="menuitem"
189
+ grep -rn '<div' src/ | grep -i 'menu-item\|menuitem\|menu__item' | grep -v 'role='
190
+
191
+ # Trigger missing aria-haspopup
192
+ grep -rn 'aria-expanded' src/ | grep -i 'menu\|dropdown' | grep -v 'aria-haspopup'
193
+
194
+ # Missing aria-labelledby on menu container
195
+ grep -rn 'role="menu"' src/ | grep -v 'aria-labelledby\|aria-label'
196
+ ```
197
+
198
+ ---
199
+
200
+ ## Failing Example
201
+
202
+ ```html
203
+ <!-- BAD: div list with click handlers but no ARIA roles -->
204
+ <div class="dropdown-menu">
205
+ <div class="dropdown-item" onclick="handleEdit()">Edit</div>
206
+ <div class="dropdown-item" onclick="handleDelete()">Delete</div>
207
+ </div>
208
+ ```
209
+
210
+ **Why it fails**: No `role="menu"` or `role="menuitem"` — screen readers cannot announce this as a menu; items are not keyboard-navigable; no arrow-key navigation; trigger lacks `aria-haspopup` and `aria-expanded`.
211
+ **Grep detection**: `grep -rn '<div.*onclick\|<div.*onClick' src/ | grep -i 'menu\|dropdown'`
212
+ **Fix**: Use `<ul role="menu">` with `<li role="menuitem" tabindex="-1">` items, or a headless menu primitive (Radix DropdownMenu, Downshift).