@hegemonart/get-design-done 1.16.0 → 1.19.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 +12 -4
- package/.claude-plugin/plugin.json +22 -4
- package/CHANGELOG.md +111 -0
- package/README.md +27 -2
- package/agents/design-auditor.md +65 -1
- package/agents/design-context-builder.md +6 -1
- package/agents/design-doc-writer.md +21 -0
- package/agents/design-executor.md +22 -4
- package/agents/design-pattern-mapper.md +62 -0
- package/agents/design-phase-researcher.md +1 -1
- package/agents/motion-mapper.md +74 -9
- package/agents/token-mapper.md +8 -0
- package/package.json +16 -2
- package/reference/components/README.md +27 -23
- package/reference/components/alert.md +198 -0
- package/reference/components/badge.md +202 -0
- package/reference/components/breadcrumbs.md +198 -0
- package/reference/components/chip.md +209 -0
- package/reference/components/command-palette.md +228 -0
- package/reference/components/date-picker.md +227 -0
- package/reference/components/file-upload.md +219 -0
- package/reference/components/list.md +217 -0
- package/reference/components/menu.md +212 -0
- package/reference/components/navbar.md +211 -0
- package/reference/components/pagination.md +205 -0
- package/reference/components/progress.md +210 -0
- package/reference/components/rich-text-editor.md +226 -0
- package/reference/components/sidebar.md +211 -0
- package/reference/components/skeleton.md +197 -0
- package/reference/components/slider.md +208 -0
- package/reference/components/stepper.md +220 -0
- package/reference/components/table.md +229 -0
- package/reference/components/toast.md +200 -0
- package/reference/components/tree.md +225 -0
- package/reference/css-grid-layout.md +835 -0
- package/reference/data-visualization.md +333 -0
- package/reference/external/NOTICE.hyperframes +28 -0
- package/reference/form-patterns.md +245 -0
- package/reference/image-optimization.md +582 -0
- package/reference/information-architecture.md +255 -0
- package/reference/motion-advanced.md +754 -0
- package/reference/motion-easings.md +381 -0
- package/reference/motion-interpolate.md +282 -0
- package/reference/motion-spring.md +234 -0
- package/reference/motion-transition-taxonomy.md +155 -0
- package/reference/motion.md +20 -0
- package/reference/onboarding-progressive-disclosure.md +250 -0
- package/reference/output-contracts/motion-map.schema.json +135 -0
- package/reference/platforms.md +346 -0
- package/reference/registry.json +445 -220
- package/reference/registry.schema.json +4 -0
- package/reference/rtl-cjk-cultural.md +353 -0
- package/reference/user-research.md +360 -0
- package/reference/variable-fonts-loading.md +532 -0
- package/scripts/lib/easings.cjs +280 -0
- package/scripts/lib/parse-contract.cjs +220 -0
- package/scripts/lib/spring.cjs +160 -0
- package/scripts/tests/test-motion-provenance.sh +64 -0
|
@@ -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).
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Navbar (Top Navigation Bar) — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Material 3, Carbon, Polaris, Atlassian, Primer, UUPM (app-interface, MIT)
|
|
4
|
+
**Wave**: 4 · **Category**: Navigation
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
A navbar is the primary horizontal navigation surface at the top of an application. It houses the logo/home link, primary navigation destinations, and secondary actions (search, notifications, profile). It communicates the application's identity and provides always-visible wayfinding. Differs from a Sidebar (vertical, secondary nav) and Breadcrumb (trail-based context). *(Material 3, Carbon, Polaris, Atlassian agree: top navbar = primary navigation + brand identity)*
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Anatomy
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
┌──────────────────────────────────────────────────────┐ role="banner"
|
|
18
|
+
│ [Skip to main] (visually hidden, focus-visible) │
|
|
19
|
+
│ [Logo/Home] | Nav Item · Nav Item · Nav Item | [🔍][👤]│ role="navigation" aria-label="Primary"
|
|
20
|
+
└──────────────────────────────────────────────────────┘
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
| Part | Required | Notes |
|
|
24
|
+
|------|----------|-------|
|
|
25
|
+
| `<header>` wrapper | Yes | `role="banner"` — one per page |
|
|
26
|
+
| `<nav>` | Yes | `role="navigation"` + `aria-label="Primary"` |
|
|
27
|
+
| Skip-to-main link | Yes | First focusable element; `href="#main-content"`; visible on focus |
|
|
28
|
+
| Logo / home link | Yes | `<a href="/">` with `aria-label="[App name] home"` |
|
|
29
|
+
| Nav items | Yes | `<a>` for routing; `aria-current="page"` on active item |
|
|
30
|
+
| Secondary actions | No | Icon buttons (search, notifications, profile) |
|
|
31
|
+
| Hamburger button | Conditional | Mobile only; `aria-expanded` + `aria-controls` |
|
|
32
|
+
| Scroll shadow | No | `box-shadow` appears on scroll > 0px |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Variants
|
|
37
|
+
|
|
38
|
+
| Variant | Description | Systems |
|
|
39
|
+
|---------|-------------|---------|
|
|
40
|
+
| Default / static | Fixed in document flow; scrolls with page | Carbon, Polaris |
|
|
41
|
+
| Sticky / fixed | `position: sticky` or `fixed`; stays at top on scroll | Material 3, Atlassian, Primer |
|
|
42
|
+
| Transparent hero | Transparent over hero image; becomes opaque on scroll | Material 3, Polaris |
|
|
43
|
+
| Compact / dense | Reduced height (48px) for data-heavy apps | Carbon, UUPM app-interface |
|
|
44
|
+
| Dashboard navbar | App-switcher + user avatar + notifications | UUPM app-interface (MIT) |
|
|
45
|
+
| Settings navbar | Breadcrumb-style subtitle below app name | UUPM app-interface (MIT) |
|
|
46
|
+
|
|
47
|
+
**Norm** (≥4 systems agree): 56–64px height; logo left-aligned; secondary actions right-aligned.
|
|
48
|
+
**Diverge**: Material 3 distinguishes "Top app bar" (mobile) from "Navigation bar" (desktop) as separate components; other systems use a single responsive navbar.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## States
|
|
53
|
+
|
|
54
|
+
| State | Trigger | Visual | ARIA |
|
|
55
|
+
|-------|---------|--------|------|
|
|
56
|
+
| default | — | Full nav visible, no shadow | — |
|
|
57
|
+
| scrolled | scroll > 0 | `box-shadow` bottom border appears | — |
|
|
58
|
+
| nav-item-hover | pointer over item | underline or bg highlight | — |
|
|
59
|
+
| nav-item-focus | keyboard focus | 2px focus-visible ring | — |
|
|
60
|
+
| nav-item-active | current route | `font-weight: 600` + indicator underline or pill | `aria-current="page"` |
|
|
61
|
+
| mobile-collapsed | viewport < breakpoint | Nav items hidden; hamburger visible | `aria-expanded="false"` on hamburger |
|
|
62
|
+
| mobile-expanded | hamburger activated | Nav items shown as vertical list | `aria-expanded="true"` on hamburger |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Sizing & Spacing
|
|
67
|
+
|
|
68
|
+
| Size | Height | Logo H | Item padding H | Font |
|
|
69
|
+
|------|--------|--------|----------------|------|
|
|
70
|
+
| compact | 48px | 24px | 12px | 13px/500 |
|
|
71
|
+
| default | 56px | 28px | 16px | 14px/500 |
|
|
72
|
+
| comfortable | 64px | 32px | 20px | 15px/500 |
|
|
73
|
+
|
|
74
|
+
**Norm**: 56px default height (Material 3, Carbon, Polaris, Atlassian all converge here).
|
|
75
|
+
Mobile breakpoint: collapse at ≤768px (Carbon, Polaris); ≤960px for complex navbars (Atlassian).
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Typography
|
|
80
|
+
|
|
81
|
+
- Nav item labels: body-sm to body-md (13–15px), weight 500 for active, 400 for inactive
|
|
82
|
+
- Active item visual distinction must not rely on color alone (add font-weight or underline indicator)
|
|
83
|
+
- Secondary action icons: 20px, with `aria-label` on each button
|
|
84
|
+
- Skip link text: `body-sm`, matches surrounding text — it only needs to be visible on focus
|
|
85
|
+
|
|
86
|
+
Cross-link: `reference/typography.md` — body scale, weight tokens
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Keyboard & Accessibility
|
|
91
|
+
|
|
92
|
+
> **WAI-ARIA role**: `banner` (header), `navigation` (nav)
|
|
93
|
+
> **Required attributes**: `aria-label="Primary"` on `<nav>`; `aria-current="page"` on active item; `aria-expanded` + `aria-controls` on hamburger
|
|
94
|
+
|
|
95
|
+
### Keyboard Contract
|
|
96
|
+
|
|
97
|
+
*Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/landmark-regions/ — W3C — 2024*
|
|
98
|
+
|
|
99
|
+
| Key | Action |
|
|
100
|
+
|-----|--------|
|
|
101
|
+
| Tab | Moves focus through focusable nav elements in DOM order |
|
|
102
|
+
| Shift+Tab | Moves focus backwards |
|
|
103
|
+
| Enter / Space | Activates focused link or button |
|
|
104
|
+
| Escape | Closes mobile expanded menu; returns focus to hamburger |
|
|
105
|
+
|
|
106
|
+
### Accessibility Rules
|
|
107
|
+
|
|
108
|
+
- `<nav>` MUST have `aria-label="Primary"` — multiple `<nav>` landmarks on a page must all be distinctly labelled
|
|
109
|
+
- Skip-to-main link MUST be the first focusable element on the page — keyboard users need to bypass repetitive nav
|
|
110
|
+
- Active nav item MUST use `aria-current="page"` — color alone is insufficient for AT users
|
|
111
|
+
- Hamburger button MUST have `aria-expanded` and `aria-controls` — AT users need to know the nav state
|
|
112
|
+
- `role="banner"` MUST appear only once per page (one `<header>` at page level)
|
|
113
|
+
|
|
114
|
+
Cross-link: `reference/accessibility.md` — landmark regions, skip navigation
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Motion
|
|
119
|
+
|
|
120
|
+
| Transition | Duration | Easing | Notes |
|
|
121
|
+
|------------|----------|--------|-------|
|
|
122
|
+
| Scroll shadow appear | 150ms | ease-out | `box-shadow` opacity only |
|
|
123
|
+
| Mobile menu open | 200ms | ease-out | Height expand or slide-down |
|
|
124
|
+
| Mobile menu close | 150ms | ease-in | Collapse |
|
|
125
|
+
| Transparent → opaque on scroll | 200ms | ease-out | Background-color transition |
|
|
126
|
+
|
|
127
|
+
**BAN**: Animating navbar height on scroll — causes layout reflow and jank on every scroll event.
|
|
128
|
+
|
|
129
|
+
Cross-link: `reference/motion.md` — layout-affecting transitions
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Do / Don't
|
|
134
|
+
|
|
135
|
+
### Do
|
|
136
|
+
- Include a visible skip-to-main link as the first focusable element *(WCAG 2.4.1, Carbon, Polaris)*
|
|
137
|
+
- Label `<nav>` with `aria-label="Primary"` *(WAI-ARIA APG landmark regions)*
|
|
138
|
+
- Use `aria-current="page"` on the active nav item *(WAI-ARIA, Atlassian)*
|
|
139
|
+
- Manage z-index explicitly on sticky navbars to prevent overlap issues *(Material 3, Carbon)*
|
|
140
|
+
|
|
141
|
+
### Don't
|
|
142
|
+
- Don't use multiple unlabelled `<nav>` landmarks — screen reader users can't distinguish them *(WCAG 4.1.2)*
|
|
143
|
+
- Don't rely on color alone to indicate the active nav item *(WCAG 1.4.1)*
|
|
144
|
+
- Don't put more than 7 primary nav items — overwhelming and hard to scan *(Carbon, Polaris HIG)*
|
|
145
|
+
- Don't use `position: fixed` on mobile without accounting for virtual keyboard displacement
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Anti-patterns Cross-links
|
|
150
|
+
|
|
151
|
+
| Anti-pattern | Entry |
|
|
152
|
+
|--------------|-------|
|
|
153
|
+
| BAN-04 | `transition: all` on interactive elements — `reference/anti-patterns.md#ban-04` |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Benchmark Citations
|
|
158
|
+
|
|
159
|
+
| Claim | Sources |
|
|
160
|
+
|-------|---------|
|
|
161
|
+
| role="banner" on outer header | WAI-ARIA APG landmark regions |
|
|
162
|
+
| aria-label="Primary" required on nav | WAI-ARIA APG, WCAG 4.1.2 |
|
|
163
|
+
| Skip-to-main as first focusable element | WCAG 2.4.1, Carbon, Polaris |
|
|
164
|
+
| aria-current="page" on active item | WAI-ARIA APG, Atlassian, Primer |
|
|
165
|
+
| 56px default navbar height | Material 3, Carbon, Polaris, Atlassian |
|
|
166
|
+
|
|
167
|
+
Full system URLs: `connections/design-corpora.md`
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Grep Signatures
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# nav element missing aria-label
|
|
175
|
+
grep -rn '<nav' src/ | grep -v 'aria-label\|aria-labelledby'
|
|
176
|
+
|
|
177
|
+
# Active nav item missing aria-current
|
|
178
|
+
grep -rn 'active\|current\|selected' src/ | grep -i 'nav.*item\|navitem\|nav-link' | grep -v 'aria-current'
|
|
179
|
+
|
|
180
|
+
# Missing skip link
|
|
181
|
+
grep -rn 'skip.*main\|skip.*content\|skipnav' src/ | grep -v 'href'
|
|
182
|
+
# If the above returns nothing, no skip link exists
|
|
183
|
+
grep -rn 'id="main\|id="main-content"' src/ | head -5
|
|
184
|
+
|
|
185
|
+
# Multiple unlabelled nav landmarks
|
|
186
|
+
grep -rn '<nav' src/ | grep -v 'aria-label\|aria-labelledby'
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Failing Example
|
|
192
|
+
|
|
193
|
+
```html
|
|
194
|
+
<!-- BAD: multiple <nav> elements without aria-label -->
|
|
195
|
+
<header>
|
|
196
|
+
<nav>
|
|
197
|
+
<a href="/">Home</a>
|
|
198
|
+
<a href="/about">About</a>
|
|
199
|
+
<a href="/contact" class="active">Contact</a> <!-- no aria-current -->
|
|
200
|
+
</nav>
|
|
201
|
+
</header>
|
|
202
|
+
<aside>
|
|
203
|
+
<nav> <!-- second nav, also unlabelled — ambiguous for AT users -->
|
|
204
|
+
<a href="/settings">Settings</a>
|
|
205
|
+
</nav>
|
|
206
|
+
</aside>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Why it fails**: Screen readers announce "navigation" twice with no way to distinguish them; active state relies on CSS class only (not `aria-current="page"`); no skip link; `<header>` lacks landmark context.
|
|
210
|
+
**Grep detection**: `grep -rn '<nav' src/ | grep -v 'aria-label'`
|
|
211
|
+
**Fix**: Add `aria-label="Primary"` and `aria-label="Secondary"` to each `<nav>`; add `aria-current="page"` to active link; add skip link as first child of `<header>`.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Pagination — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Carbon, Polaris, Atlassian, Mantine, Material 3, UUPM (app-interface, MIT)
|
|
4
|
+
**Wave**: 4 · **Category**: Navigation
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Pagination divides a large dataset into discrete pages and provides controls to navigate between them. It is the preferred pattern for server-rendered or API-paginated datasets where loading all items at once is impractical. Use infinite scroll for feeds/social content; use pagination for tables, search results, and list views where users need to reference or return to a specific page. *(Carbon, Polaris, Atlassian, Mantine all define pagination as discrete page-set navigation)*
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Anatomy
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
<nav aria-label="Pagination">
|
|
18
|
+
[‹ Previous] [1] [2] [•3•] [4] [...] [24] [Next ›]
|
|
19
|
+
↑ aria-current="page"
|
|
20
|
+
Items per page: [25 ▾] Showing 51–75 of 587 items
|
|
21
|
+
</nav>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Part | Required | Notes |
|
|
25
|
+
|------|----------|-------|
|
|
26
|
+
| `<nav>` container | Yes | `aria-label="Pagination"` |
|
|
27
|
+
| Previous button | Yes | `aria-label="Previous page"`; `disabled` on page 1 |
|
|
28
|
+
| Next button | Yes | `aria-label="Next page"`; `disabled` on last page |
|
|
29
|
+
| Page number buttons | Yes | Each button `aria-label="Page N"` |
|
|
30
|
+
| Current page indicator | Yes | `aria-current="page"` on active page button |
|
|
31
|
+
| Ellipsis | Conditional | At > 7 pages; `aria-hidden="true"` or `aria-label="More pages"` |
|
|
32
|
+
| Per-page selector | No | `<select>` with visible `<label>`; options: 10/25/50/100 |
|
|
33
|
+
| Results summary | No | "Showing 51–75 of 587 items"; `aria-live="polite"` on change |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Variants
|
|
38
|
+
|
|
39
|
+
| Variant | Description | Systems |
|
|
40
|
+
|---------|-------------|---------|
|
|
41
|
+
| Full | Previous + page numbers + Next | Carbon, Polaris, Atlassian, Mantine |
|
|
42
|
+
| Compact | Previous + "Page N of M" label + Next (no individual page buttons) | Carbon, Material 3 |
|
|
43
|
+
| Simple | Previous / Next only (no page numbers) | Polaris, Atlassian mobile |
|
|
44
|
+
| With per-page | Adds items-per-page selector below or beside | Carbon, Polaris, UUPM list-view (MIT) |
|
|
45
|
+
| Borderless | Text-only Previous/Next; no page number buttons | Mantine |
|
|
46
|
+
|
|
47
|
+
**Norm** (≥4 systems agree): show 5–7 page buttons max when not truncating; always show first, last, ±1 around current when truncating.
|
|
48
|
+
**Diverge**: Carbon combines per-page selector and result count into the pagination bar; Polaris separates them.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## States
|
|
53
|
+
|
|
54
|
+
| State | Trigger | Visual | ARIA |
|
|
55
|
+
|-------|---------|--------|------|
|
|
56
|
+
| default | — | All buttons enabled except current page | — |
|
|
57
|
+
| current-page | — | Filled/highlighted button | `aria-current="page"` |
|
|
58
|
+
| prev-disabled | page = 1 | Previous button dimmed | `disabled` or `aria-disabled="true"` |
|
|
59
|
+
| next-disabled | page = last | Next button dimmed | `disabled` or `aria-disabled="true"` |
|
|
60
|
+
| button-hover | pointer over | 8% overlay | — |
|
|
61
|
+
| button-focus | keyboard focus | 2px focus-visible ring | — |
|
|
62
|
+
| loading | page change in flight | Spinner overlay on content; buttons retain focus | `aria-busy="true"` on results region |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Sizing & Spacing
|
|
67
|
+
|
|
68
|
+
| Element | Size | Notes |
|
|
69
|
+
|---------|------|-------|
|
|
70
|
+
| Page button | 36×36px (md) | 32×32px compact; min tap target 44px via padding |
|
|
71
|
+
| Prev/Next button | 36px height × auto | Label text + chevron icon |
|
|
72
|
+
| Button gap | 4px | Between adjacent page buttons |
|
|
73
|
+
| Per-page select | 80px width | 4 options: 10/25/50/100 |
|
|
74
|
+
| Container padding | 16px V | Breathing room above/below |
|
|
75
|
+
|
|
76
|
+
**Norm**: 36px button height (Carbon, Atlassian, Mantine). 4px gap between buttons.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Typography
|
|
81
|
+
|
|
82
|
+
- Page number buttons: body-sm (13–14px), weight 400; active page weight 600
|
|
83
|
+
- Previous/Next labels: body-sm, weight 400
|
|
84
|
+
- Results summary: body-sm, `color: --text-subtle`
|
|
85
|
+
- Per-page label: label-sm (12px), always visible above or beside the select
|
|
86
|
+
|
|
87
|
+
Cross-link: `reference/typography.md` — body-sm, label scale
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Keyboard & Accessibility
|
|
92
|
+
|
|
93
|
+
> **WAI-ARIA role**: `navigation` (container)
|
|
94
|
+
> **Required attributes**: `aria-label="Pagination"` on `<nav>`; `aria-current="page"` on active page button; `aria-label="Previous page"` / `"Next page"` on controls; `aria-label="Page N"` on each numbered button
|
|
95
|
+
|
|
96
|
+
### Keyboard Contract
|
|
97
|
+
|
|
98
|
+
*Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/landmark-regions/ — W3C — 2024*
|
|
99
|
+
|
|
100
|
+
| Key | Action |
|
|
101
|
+
|-----|--------|
|
|
102
|
+
| Tab | Moves focus to next button in pagination |
|
|
103
|
+
| Shift+Tab | Moves focus to previous button |
|
|
104
|
+
| Enter / Space | Activates focused button (navigates to page or changes per-page count) |
|
|
105
|
+
|
|
106
|
+
### Accessibility Rules
|
|
107
|
+
|
|
108
|
+
- `aria-label="Pagination"` MUST be on the `<nav>` — distinguishes from other navigation landmarks on the page
|
|
109
|
+
- `aria-current="page"` MUST be on the currently active page button — screen readers announce "Page 3, current"
|
|
110
|
+
- Previous button on page 1 and Next button on last page MUST use `disabled` or `aria-disabled="true"` + visually dimmed
|
|
111
|
+
- `aria-label` on each page number button ("Page 1", "Page 2") prevents screen readers from just announcing the number alone
|
|
112
|
+
- Results summary text MUST use `aria-live="polite"` so screen reader users hear the count update after page change
|
|
113
|
+
- Per-page `<select>` MUST have a visible `<label>` — not just a placeholder
|
|
114
|
+
|
|
115
|
+
Cross-link: `reference/accessibility.md` — aria-current, landmark labelling, live regions
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Motion
|
|
120
|
+
|
|
121
|
+
| Transition | Duration | Easing | Notes |
|
|
122
|
+
|------------|----------|--------|-------|
|
|
123
|
+
| Button hover | 80ms | ease-out | Background color only |
|
|
124
|
+
| Page change content transition | 150ms | ease-out | Fade the results region; managed by page, not pagination |
|
|
125
|
+
| Ellipsis expand (if interactive) | 120ms | ease-out | Reveal hidden page buttons |
|
|
126
|
+
|
|
127
|
+
**BAN**: Do not animate the pagination bar itself when a page changes — only the content region transitions.
|
|
128
|
+
|
|
129
|
+
Cross-link: `reference/motion.md` — content region transitions
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Do / Don't
|
|
134
|
+
|
|
135
|
+
### Do
|
|
136
|
+
- Label the `<nav>` with `aria-label="Pagination"` *(WAI-ARIA APG, Carbon, Polaris)*
|
|
137
|
+
- Use `aria-current="page"` on the active page button *(WAI-ARIA APG)*
|
|
138
|
+
- Provide visible per-page selector with a visible label *(Carbon, Polaris, Atlassian)*
|
|
139
|
+
- Show a results summary ("Showing 51–75 of 587") near the pagination controls *(Carbon, Polaris)*
|
|
140
|
+
|
|
141
|
+
### Don't
|
|
142
|
+
- Don't use `<a href>` with pushState without `aria-current` — screen readers won't know which page is current *(WAI-ARIA)*
|
|
143
|
+
- Don't show more than 7 page buttons without ellipsis truncation — visually overwhelming *(Carbon, Mantine)*
|
|
144
|
+
- Don't disable the Previous/Next buttons with only visual styling — use `disabled` attr or `aria-disabled` *(WCAG 1.4.3)*
|
|
145
|
+
- Don't place pagination at the top only — bottom placement is expected; both positions is acceptable *(Polaris, Carbon)*
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Anti-patterns Cross-links
|
|
150
|
+
|
|
151
|
+
| Anti-pattern | Entry |
|
|
152
|
+
|--------------|-------|
|
|
153
|
+
| BAN-07 | Missing `aria-current` on active navigation items — `reference/anti-patterns.md#ban-07` |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Benchmark Citations
|
|
158
|
+
|
|
159
|
+
| Claim | Sources |
|
|
160
|
+
|-------|---------|
|
|
161
|
+
| aria-label="Pagination" on nav container | WAI-ARIA APG, Carbon, Polaris |
|
|
162
|
+
| aria-current="page" on active page button | WAI-ARIA APG, Atlassian, Mantine |
|
|
163
|
+
| Ellipsis pattern: first + last + ±1 around current | Carbon, Polaris, Atlassian, Mantine |
|
|
164
|
+
| Per-page selector requires visible label | WCAG 1.3.1, Carbon, Polaris |
|
|
165
|
+
| aria-live="polite" on results summary | WCAG 4.1.3, Carbon |
|
|
166
|
+
|
|
167
|
+
Full system URLs: `connections/design-corpora.md`
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Grep Signatures
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Pagination nav missing aria-label
|
|
175
|
+
grep -rn 'pagination\|pager' src/ | grep '<nav' | grep -v 'aria-label'
|
|
176
|
+
|
|
177
|
+
# Active page button missing aria-current
|
|
178
|
+
grep -rn 'pagination\|pager' src/ | grep 'active\|current\|selected' | grep -v 'aria-current'
|
|
179
|
+
|
|
180
|
+
# Page buttons using <a> without aria-current
|
|
181
|
+
grep -rn '<a' src/ | grep -i 'page.*[0-9]\|pager' | grep -v 'aria-current'
|
|
182
|
+
|
|
183
|
+
# Per-page select missing label
|
|
184
|
+
grep -rn 'per.page\|items-per-page\|pageSize' src/ | grep '<select' | grep -v '<label\|aria-label'
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Failing Example
|
|
190
|
+
|
|
191
|
+
```html
|
|
192
|
+
<!-- BAD: page buttons using <a href> with pushState but no aria-current -->
|
|
193
|
+
<nav> <!-- missing aria-label="Pagination" -->
|
|
194
|
+
<a href="?page=2" class="prev">Previous</a> <!-- no aria-label -->
|
|
195
|
+
<a href="?page=1">1</a>
|
|
196
|
+
<a href="?page=2">2</a>
|
|
197
|
+
<a href="?page=3" class="active">3</a> <!-- class active but no aria-current -->
|
|
198
|
+
<a href="?page=4">4</a>
|
|
199
|
+
<a href="?page=4" class="next">Next</a> <!-- no aria-label -->
|
|
200
|
+
</nav>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Why it fails**: No `aria-label` on `<nav>`; active page has CSS class but no `aria-current="page"` so AT announces "3" not "Page 3, current"; Previous/Next have no descriptive labels; no `aria-label="Page N"` on individual page links.
|
|
204
|
+
**Grep detection**: `grep -rn 'pagination' src/ | grep -v 'aria-current\|aria-label'`
|
|
205
|
+
**Fix**: Add `aria-label="Pagination"` to `<nav>`; add `aria-current="page"` to the active page button; add `aria-label="Previous page"` and `aria-label="Next page"`; add `aria-label="Page N"` to each numbered button.
|