@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,184 @@
|
|
|
1
|
+
# [Component Name] — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
> **Template version**: 1.0 (Phase 16)
|
|
4
|
+
> Replace every placeholder in `[brackets]`. Delete this block before committing.
|
|
5
|
+
> Max 350 lines. Every section must be present even if brief.
|
|
6
|
+
> See `agents/component-benchmark-synthesizer.md` for authoring rules.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
**Harvested from**: [N] design systems · [date]
|
|
11
|
+
**Wave**: [1 | 2 | 3 | 4 | 5] · **Category**: [Inputs | Containers | Feedback | Navigation | Advanced]
|
|
12
|
+
**Spec file**: `reference/components/[name].md`
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Purpose
|
|
17
|
+
|
|
18
|
+
[One paragraph: what this component is, its core job, and when to use it vs. alternatives.
|
|
19
|
+
Cite convergence: "(Material 3, Carbon, Polaris agree: …)"]
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Anatomy
|
|
24
|
+
|
|
25
|
+
[Describe the structural parts. Use a numbered or bulleted list of named elements.
|
|
26
|
+
Mark each as optional or required.]
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
[ASCII diagram or bulleted tree showing component structure]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Part | Required | Notes |
|
|
33
|
+
|------|----------|-------|
|
|
34
|
+
| [part] | Yes/No | [brief description] |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Variants
|
|
39
|
+
|
|
40
|
+
[List the canonical variant set. For each, note which systems define it and what
|
|
41
|
+
distinguishes it visually/behaviorally.]
|
|
42
|
+
|
|
43
|
+
| Variant | Description | Systems |
|
|
44
|
+
|---------|-------------|---------|
|
|
45
|
+
| [variant] | [what it is] | [Material 3, Carbon, …] |
|
|
46
|
+
|
|
47
|
+
**Norm** (≥4/18 systems agree): [what the majority agrees on for variant naming/behavior]
|
|
48
|
+
**Diverge**: [where systems meaningfully differ and why]
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## States
|
|
53
|
+
|
|
54
|
+
[All interaction states the component must handle. Include visual and semantic description.]
|
|
55
|
+
|
|
56
|
+
| State | Trigger | Visual | ARIA |
|
|
57
|
+
|-------|---------|--------|------|
|
|
58
|
+
| default | — | [description] | — |
|
|
59
|
+
| hover | pointer over | [description] | — |
|
|
60
|
+
| focus | keyboard focus | focus-visible ring | — |
|
|
61
|
+
| active / pressed | mousedown / Space/Enter | [description] | — |
|
|
62
|
+
| disabled | `disabled` attr | [description] | `aria-disabled="true"` |
|
|
63
|
+
| loading | [if applicable] | [description] | `aria-busy="true"` |
|
|
64
|
+
| error | [if applicable] | [description] | `aria-invalid="true"` |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Sizing & Spacing
|
|
69
|
+
|
|
70
|
+
[Token-based sizing guidance. Reference `reference/typography.md` and `reference/surfaces.md`
|
|
71
|
+
for typographic and radius rules.]
|
|
72
|
+
|
|
73
|
+
| Size | Height | Padding H | Font | Notes |
|
|
74
|
+
|------|--------|-----------|------|-------|
|
|
75
|
+
| sm | [value] | [value] | [value] | |
|
|
76
|
+
| md (default) | [value] | [value] | [value] | |
|
|
77
|
+
| lg | [value] | [value] | [value] | |
|
|
78
|
+
|
|
79
|
+
**Norm**: [what most systems agree on for sizing ratios]
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Typography
|
|
84
|
+
|
|
85
|
+
[Any typographic constraints: weight, size relative to body, line-height cap, truncation rules.]
|
|
86
|
+
|
|
87
|
+
Cross-link: `reference/typography.md` — [relevant section]
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Keyboard & Accessibility
|
|
92
|
+
|
|
93
|
+
> **WAI-ARIA role**: `[role]`
|
|
94
|
+
> **Required attributes**: `[aria-* attrs]`
|
|
95
|
+
|
|
96
|
+
### Keyboard Contract
|
|
97
|
+
|
|
98
|
+
*Quoted verbatim from WAI-ARIA APG — [pattern URL] — W3C — [access date]*
|
|
99
|
+
|
|
100
|
+
| Key | Action |
|
|
101
|
+
|-----|--------|
|
|
102
|
+
| [key] | [action] |
|
|
103
|
+
|
|
104
|
+
### Accessibility Rules
|
|
105
|
+
|
|
106
|
+
- [Rule 1 — e.g., must have visible label or `aria-label`]
|
|
107
|
+
- [Rule 2 — e.g., focus-visible ring must not be suppressed]
|
|
108
|
+
- [Rule 3 — e.g., disabled state uses `aria-disabled`, not `disabled` attr, if interactive]
|
|
109
|
+
|
|
110
|
+
Cross-link: `reference/accessibility.md` — [relevant section]
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Motion
|
|
115
|
+
|
|
116
|
+
[Animation guidance for entry, exit, state transitions. Reference `reference/motion.md`.]
|
|
117
|
+
|
|
118
|
+
| Transition | Duration | Easing | Notes |
|
|
119
|
+
|------------|----------|--------|-------|
|
|
120
|
+
| [transition] | [ms] | [easing] | |
|
|
121
|
+
|
|
122
|
+
**BAN**: [any motion anti-patterns specific to this component]
|
|
123
|
+
|
|
124
|
+
Cross-link: `reference/motion.md` — [relevant section]
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Do / Don't
|
|
129
|
+
|
|
130
|
+
### Do
|
|
131
|
+
- [Positive practice 1] *([System])*
|
|
132
|
+
- [Positive practice 2] *([System])*
|
|
133
|
+
- [Positive practice 3] *([System])*
|
|
134
|
+
|
|
135
|
+
### Don't
|
|
136
|
+
- [Anti-practice 1] *(diverges from [N] systems)*
|
|
137
|
+
- [Anti-practice 2]
|
|
138
|
+
- [Anti-practice 3]
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Anti-patterns Cross-links
|
|
143
|
+
|
|
144
|
+
| Anti-pattern | Entry |
|
|
145
|
+
|--------------|-------|
|
|
146
|
+
| [BAN-XX] | [brief name] — `reference/anti-patterns.md#ban-xx` |
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Benchmark Citations
|
|
151
|
+
|
|
152
|
+
| Claim | Sources |
|
|
153
|
+
|-------|---------|
|
|
154
|
+
| [claim] | [System1, System2, System3] |
|
|
155
|
+
|
|
156
|
+
Full system URLs: `connections/design-corpora.md`
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Grep Signatures
|
|
161
|
+
|
|
162
|
+
Patterns to detect common implementation failures (used by `design-auditor`):
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
# Missing accessible label on icon-only variant
|
|
166
|
+
grep -rn 'aria-label\|aria-labelledby' src/ | grep -v '[Component]'
|
|
167
|
+
|
|
168
|
+
# [Additional grep pattern with comment explaining what it detects]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Failing Example
|
|
174
|
+
|
|
175
|
+
What a broken implementation of this component looks like, and how to detect it:
|
|
176
|
+
|
|
177
|
+
```[html|jsx|tsx]
|
|
178
|
+
<!-- BAD: [describe the failure] -->
|
|
179
|
+
[broken code snippet]
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Why it fails**: [explanation]
|
|
183
|
+
**Grep detection**: `grep -rn '[pattern]' src/`
|
|
184
|
+
**Fix**: [one-line fix or cross-link to reference]
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Accordion — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: WAI-ARIA APG, Radix UI, Carbon, Chakra UI, Material 3, Mantine, shadcn/ui, Atlassian
|
|
4
|
+
**Wave**: 2 · **Category**: Containers
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
An accordion is a vertically stacked set of headers that each reveal or conceal an associated section of content when activated. It reduces visual clutter by hiding content that is not immediately relevant. The header MUST be a button (or have `role="button"`) — users must be able to activate sections by keyboard. *(WAI-ARIA APG, Radix, Carbon all agree)*
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Anatomy
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
┌──────────────────────────────┬──┐
|
|
18
|
+
│ Section 1 header (button) │ ▾│ ← aria-expanded="true"
|
|
19
|
+
└──────────────────────────────┴──┘
|
|
20
|
+
┌────────────────────────────┐
|
|
21
|
+
│ Section 1 content │ ← aria-hidden="false"
|
|
22
|
+
└────────────────────────────┘
|
|
23
|
+
┌──────────────────────────────┬──┐
|
|
24
|
+
│ Section 2 header (button) │ ▾│ ← aria-expanded="false"
|
|
25
|
+
└──────────────────────────────┴──┘
|
|
26
|
+
(Section 2 content hidden)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| Part | Required | Notes |
|
|
30
|
+
|------|----------|-------|
|
|
31
|
+
| Header (h2–h6) | Yes | Wraps the trigger button; maintains document outline |
|
|
32
|
+
| Trigger button | Yes | `<button>` with `aria-expanded` + `aria-controls` |
|
|
33
|
+
| Panel | Yes | `id` referenced by `aria-controls`; `role="region"` + `aria-labelledby` |
|
|
34
|
+
| Chevron / icon | No | Rotates 180° on open; `aria-hidden="true"` |
|
|
35
|
+
| Divider | No | Border between items |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Variants
|
|
40
|
+
|
|
41
|
+
| Variant | Description | Systems |
|
|
42
|
+
|---------|-------------|---------|
|
|
43
|
+
| Single-open | Only one panel open at a time | Material 3, Mantine (default) |
|
|
44
|
+
| Multi-open | Multiple panels can be open simultaneously | Radix (default), Carbon, shadcn |
|
|
45
|
+
| Bordered | Each item has a border/card appearance | All |
|
|
46
|
+
| Flush / ghost | No border; full-width dividers only | shadcn, Material 3 |
|
|
47
|
+
| With icon | Leading icon per header | Carbon, Atlassian |
|
|
48
|
+
|
|
49
|
+
**Norm** (≥5/18): chevron icon rotates on open (180°); panel animates height.
|
|
50
|
+
**Diverge**: single-open vs. multi-open — Radix defaults to multi-open (more flexible); Material 3 defaults to single-open (simpler UX). Multi-open is safer for long FAQ pages where users may want to compare sections.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## States
|
|
55
|
+
|
|
56
|
+
| State | Trigger | Visual | ARIA |
|
|
57
|
+
|-------|---------|--------|------|
|
|
58
|
+
| collapsed | default | Panel hidden | `aria-expanded="false"` on button |
|
|
59
|
+
| expanded | button activated | Panel visible | `aria-expanded="true"` on button |
|
|
60
|
+
| hover | pointer over header | Header background tint | — |
|
|
61
|
+
| focus | keyboard | 2px focus ring on button | — |
|
|
62
|
+
| disabled | programmatic | 38% opacity; non-interactive | `aria-disabled="true"` on button |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Sizing & Spacing
|
|
67
|
+
|
|
68
|
+
| Property | Value | Notes |
|
|
69
|
+
|----------|-------|-------|
|
|
70
|
+
| Header height | 48px (md default) | Touch-friendly |
|
|
71
|
+
| Padding H (header) | 16px | |
|
|
72
|
+
| Padding (panel) | 16px | |
|
|
73
|
+
| Icon size | 16–20px | Chevron; gap from label: 8px |
|
|
74
|
+
| Item border | 1px `border-bottom` | Flush style |
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Typography
|
|
79
|
+
|
|
80
|
+
- Header: 14–16px/500 — slightly heavier than panel body
|
|
81
|
+
- Panel body: 14px/400
|
|
82
|
+
- Disabled header: same size, 38% opacity
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Keyboard & Accessibility
|
|
87
|
+
|
|
88
|
+
> **WAI-ARIA role**: `button` on header trigger (implicit if `<button>`); `region` on panel
|
|
89
|
+
> **Header trigger attributes**: `aria-expanded`, `aria-controls` (panel id)
|
|
90
|
+
> **Panel attributes**: `id`, `role="region"`, `aria-labelledby` (header button id)
|
|
91
|
+
|
|
92
|
+
### Keyboard Contract
|
|
93
|
+
|
|
94
|
+
*Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/accordion/ — W3C — 2024*
|
|
95
|
+
|
|
96
|
+
| Key | Action |
|
|
97
|
+
|-----|--------|
|
|
98
|
+
| Enter / Space | When focus is on the accordion header, toggles the associated panel |
|
|
99
|
+
| Tab | Moves focus to the next focusable element |
|
|
100
|
+
| Shift+Tab | Moves focus to the previous focusable element |
|
|
101
|
+
| Arrow Down | (Optional) Moves focus to the next accordion header |
|
|
102
|
+
| Arrow Up | (Optional) Moves focus to the previous accordion header |
|
|
103
|
+
| Home | (Optional) Moves focus to the first accordion header |
|
|
104
|
+
| End | (Optional) Moves focus to the last accordion header |
|
|
105
|
+
|
|
106
|
+
Arrow/Home/End navigation is optional; Tab navigation between headers is always required.
|
|
107
|
+
|
|
108
|
+
### Accessibility Rules
|
|
109
|
+
|
|
110
|
+
- Header MUST be wrapped in an `<h2>`–`<h6>` tag to maintain document outline — the heading level should match the surrounding page hierarchy
|
|
111
|
+
- The trigger MUST be a `<button>` (or `role="button"`) — `<div>` headers are inaccessible by keyboard
|
|
112
|
+
- `aria-expanded` MUST be on the trigger button, not the panel
|
|
113
|
+
- Panel SHOULD have `role="region"` + `aria-labelledby` referencing the trigger button id — this creates a named region for landmark navigation
|
|
114
|
+
- `role="region"` should be omitted if there are more than 6 accordions (too many regions = noisy landmark nav)
|
|
115
|
+
- Avoid `display:none` for the panel in open state — use `hidden` or height animation
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Motion
|
|
120
|
+
|
|
121
|
+
| Transition | Duration | Easing | Notes |
|
|
122
|
+
|------------|----------|--------|-------|
|
|
123
|
+
| Panel expand | 200ms | ease-out | Height 0→auto via `grid-template-rows` trick |
|
|
124
|
+
| Panel collapse | 150ms | ease-in | |
|
|
125
|
+
| Chevron rotate | 200ms | ease | 0→180deg |
|
|
126
|
+
|
|
127
|
+
**Height animation trick** (CSS-only, no JS measurement):
|
|
128
|
+
```css
|
|
129
|
+
.panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 200ms ease; }
|
|
130
|
+
.panel.open { grid-template-rows: 1fr; }
|
|
131
|
+
.panel > div { overflow: hidden; }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Cross-link: `reference/motion.md` — `prefers-reduced-motion`: skip height animation, instant toggle
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Do / Don't
|
|
139
|
+
|
|
140
|
+
### Do
|
|
141
|
+
- Wrap header triggers in proper heading tags (`<h2>`–`<h6>`) *(WAI-ARIA APG, Carbon)*
|
|
142
|
+
- Use `aria-expanded` on the trigger button, not on the panel *(WAI-ARIA APG)*
|
|
143
|
+
- Animate height with `grid-template-rows` trick — no JS height measurement needed *(CSS-only pattern)*
|
|
144
|
+
- Support Tab navigation between accordion headers at minimum *(WAI-ARIA APG)*
|
|
145
|
+
|
|
146
|
+
### Don't
|
|
147
|
+
- Don't use `<div>` as the accordion header without `role="button"` *(WAI-ARIA APG)*
|
|
148
|
+
- Don't use `display:none` to hide the panel in collapsed state if you want animation — use CSS grid trick *(CSS pattern)*
|
|
149
|
+
- Don't use `role="region"` for more than 6 accordions — excessive landmarks harm screen reader navigation *(WAI-ARIA APG note)*
|
|
150
|
+
- Don't auto-close other panels in a "single-open" accordion without announcing the change *(Atlassian)*
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Anti-patterns Cross-links
|
|
155
|
+
|
|
156
|
+
| Anti-pattern | Entry |
|
|
157
|
+
|--------------|-------|
|
|
158
|
+
| div header without role="button" | `reference/anti-patterns.md` |
|
|
159
|
+
| aria-expanded on panel instead of button | `reference/anti-patterns.md` |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Benchmark Citations
|
|
164
|
+
|
|
165
|
+
| Claim | Sources |
|
|
166
|
+
|-------|---------|
|
|
167
|
+
| Header in h2–h6 required | WAI-ARIA APG, Carbon |
|
|
168
|
+
| aria-expanded on trigger (not panel) | WAI-ARIA APG §3.1 |
|
|
169
|
+
| role="region" + aria-labelledby on panel | WAI-ARIA APG |
|
|
170
|
+
| Enter/Space to toggle | WAI-ARIA APG §3.1 |
|
|
171
|
+
| grid-template-rows for height animation | CSS-only pattern (no system-specific) |
|
|
172
|
+
|
|
173
|
+
Full system URLs: `connections/design-corpora.md`
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Grep Signatures
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# Accordion header not a button
|
|
181
|
+
grep -rn 'accordion\|Accordion' src/ | grep 'header\|trigger' | grep -v '<button\|role="button"'
|
|
182
|
+
|
|
183
|
+
# aria-expanded on wrong element (panel instead of trigger)
|
|
184
|
+
grep -rn 'aria-expanded' src/ | grep 'panel\|content\|body'
|
|
185
|
+
|
|
186
|
+
# display:none used on accordion panel (breaks animation)
|
|
187
|
+
grep -rn 'accordion.*panel\|panel.*accordion' src/ | grep 'display.*none'
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Failing Example
|
|
193
|
+
|
|
194
|
+
```html
|
|
195
|
+
<!-- BAD: div header — no keyboard access, no aria-expanded -->
|
|
196
|
+
<div class="accordion-header" onclick="toggle(1)">
|
|
197
|
+
What is the return policy?
|
|
198
|
+
</div>
|
|
199
|
+
<div id="panel-1" class="accordion-panel" style="display:none">
|
|
200
|
+
Our return policy is 30 days…
|
|
201
|
+
</div>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Why it fails**: `<div>` is not keyboard-operable (no Tab stop). No `aria-expanded`. Screen reader cannot determine expanded state. `display:none` prevents CSS animation.
|
|
205
|
+
**Grep detection**: `grep -rn 'class.*accordion.*header\|accordion-header' src/ | grep -v '<button\|role="button"'`
|
|
206
|
+
**Fix**:
|
|
207
|
+
```html
|
|
208
|
+
<h3>
|
|
209
|
+
<button aria-expanded="false" aria-controls="panel-1" id="header-1">
|
|
210
|
+
What is the return policy?
|
|
211
|
+
<svg aria-hidden="true"><!-- chevron --></svg>
|
|
212
|
+
</button>
|
|
213
|
+
</h3>
|
|
214
|
+
<div id="panel-1" role="region" aria-labelledby="header-1" class="panel">
|
|
215
|
+
<div>Our return policy is 30 days…</div>
|
|
216
|
+
</div>
|
|
217
|
+
```
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Button — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Material 3, Polaris, Carbon, Fluent 2, Radix, shadcn/ui, Primer, Atlassian
|
|
4
|
+
**Wave**: 1 · **Category**: Inputs
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
A button triggers a discrete action in the current context — submitting a form, opening a dialog, executing a command. It is not a navigation element (use Link for href-based navigation). Buttons have an explicit visual affordance of clickability and must communicate their current state (loading, disabled) clearly. *(Material 3, Carbon, Polaris agree: button = action trigger, not navigation)*
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Anatomy
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
[ icon? ] [ label ] [ trailing-icon? ]
|
|
18
|
+
└── role="button" or <button>
|
|
19
|
+
└── focus-visible ring (2px offset)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
| Part | Required | Notes |
|
|
23
|
+
|------|----------|-------|
|
|
24
|
+
| Label | Yes (or `aria-label`) | Visible text preferred; `aria-label` for icon-only |
|
|
25
|
+
| Root element | Yes | Must be `<button>` or element with `role="button"` + `tabindex="0"` |
|
|
26
|
+
| Leading icon | No | Left-aligned, 16–20px, optical spacing ~8px |
|
|
27
|
+
| Trailing icon | No | Right-aligned; use sparingly (chevron for split buttons) |
|
|
28
|
+
| Focus ring | Yes | 2px solid, 2px offset from border; never hidden |
|
|
29
|
+
| Loading indicator | No | Spinner replaces or overlays label; `aria-busy="true"` |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Variants
|
|
34
|
+
|
|
35
|
+
| Variant | Description | Systems |
|
|
36
|
+
|---------|-------------|---------|
|
|
37
|
+
| Primary / Filled | Highest emphasis; one per view section | Material 3, Carbon, Polaris, Fluent, Primer |
|
|
38
|
+
| Secondary / Outlined | Medium emphasis; alternate action | Material 3, Carbon, Polaris, Fluent |
|
|
39
|
+
| Ghost / Text | Low emphasis; tertiary or inline actions | Material 3 (text), Carbon (ghost), Polaris (plain) |
|
|
40
|
+
| Destructive | Irreversible actions (delete, remove) | Polaris (critical), Carbon (danger), shadcn |
|
|
41
|
+
| Icon-only | No visible label; requires `aria-label` | All systems |
|
|
42
|
+
| Link-style | Looks like link, behaves as button | Carbon, Primer |
|
|
43
|
+
|
|
44
|
+
**Norm** (≥6/18 systems agree): primary/secondary/ghost hierarchy; one primary per viewport section.
|
|
45
|
+
**Diverge**: "tertiary" naming (Material 3) vs. "ghost" (Carbon) vs. "plain" (Polaris) — same visual intent, different labels.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## States
|
|
50
|
+
|
|
51
|
+
| State | Trigger | Visual | ARIA |
|
|
52
|
+
|-------|---------|--------|------|
|
|
53
|
+
| default | — | Resting fill/border | — |
|
|
54
|
+
| hover | pointer over | 8% overlay (light) / 8% overlay (dark) | — |
|
|
55
|
+
| focus | keyboard focus | 2px focus-visible ring, 2px offset | — |
|
|
56
|
+
| active / pressed | mousedown / Space / Enter | 12% overlay; scale 0.96 | — |
|
|
57
|
+
| disabled | `disabled` attr | 38% opacity; cursor: not-allowed | `disabled` attr |
|
|
58
|
+
| loading | async action in flight | Spinner, `aria-busy="true"` | `aria-busy="true"` |
|
|
59
|
+
|
|
60
|
+
**Norm**: 96% scale on press (Material 3, shadcn, Carbon confirm). 38% opacity for disabled (Material 3 spec).
|
|
61
|
+
**Diverge**: hover overlay vs. background tint — systems use either approach; overlay is more theme-portable.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Sizing & Spacing
|
|
66
|
+
|
|
67
|
+
| Size | Height | Padding H | Min-width | Font |
|
|
68
|
+
|------|--------|-----------|-----------|------|
|
|
69
|
+
| sm | 32px | 12px | 64px | 13px/500 |
|
|
70
|
+
| md (default) | 40px | 16px | 80px | 14px/500 |
|
|
71
|
+
| lg | 48px | 24px | 96px | 16px/500 |
|
|
72
|
+
|
|
73
|
+
**Norm**: 40px default height (Carbon, Polaris, Fluent all confirm). Min-width prevents single-character buttons.
|
|
74
|
+
Cross-link: `reference/surfaces.md` — hit-area rule (minimum 44×44px accessible tap target via padding, not height).
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Typography
|
|
79
|
+
|
|
80
|
+
- Weight: 500 (medium) — not bold; distinguishes from body text without being heavy *(Material 3, Carbon)*
|
|
81
|
+
- Letter-spacing: +0.01em for sm/md, 0 for lg
|
|
82
|
+
- No text truncation — resize button or use icon-only variant; truncated button labels break affordance
|
|
83
|
+
|
|
84
|
+
Cross-link: `reference/typography.md` — tabular-nums rule (use on loading counters, not labels)
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Keyboard & Accessibility
|
|
89
|
+
|
|
90
|
+
> **WAI-ARIA role**: `button`
|
|
91
|
+
> **Required attributes**: none if `<button>`; `role="button"` + `tabindex="0"` if `<div>`/`<span>`
|
|
92
|
+
|
|
93
|
+
### Keyboard Contract
|
|
94
|
+
|
|
95
|
+
*Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/button/ — W3C — 2024*
|
|
96
|
+
|
|
97
|
+
| Key | Action |
|
|
98
|
+
|-----|--------|
|
|
99
|
+
| Enter | Activates the button |
|
|
100
|
+
| Space | Activates the button |
|
|
101
|
+
|
|
102
|
+
### Accessibility Rules
|
|
103
|
+
|
|
104
|
+
- Icon-only buttons MUST have `aria-label` or `aria-labelledby` — a tooltip is not a substitute
|
|
105
|
+
- Loading state: set `aria-busy="true"` and disable pointer events; announce completion via `aria-live`
|
|
106
|
+
- Disabled: use native `disabled` attribute (not `aria-disabled`) unless button must remain focusable for tooltip explanation
|
|
107
|
+
- Never use `<div>` or `<a>` as a button trigger without `role="button"` and keyboard handlers
|
|
108
|
+
- Focus ring must never be `outline: none` without a visible CSS custom alternative
|
|
109
|
+
|
|
110
|
+
Cross-link: `reference/accessibility.md`
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Motion
|
|
115
|
+
|
|
116
|
+
| Transition | Duration | Easing | Notes |
|
|
117
|
+
|------------|----------|--------|-------|
|
|
118
|
+
| hover overlay | 120ms | ease-out | Subtle; background/border only |
|
|
119
|
+
| press scale (0.96) | 80ms | ease-in | Immediate tactile feedback |
|
|
120
|
+
| loading spinner in | 150ms | ease-out | Replaces or overlays label |
|
|
121
|
+
|
|
122
|
+
**BAN**: `transition: all` — catches width change on loading and causes layout jank.
|
|
123
|
+
|
|
124
|
+
Cross-link: `reference/motion.md` — canonical scale-on-press 0.96, BAN-04 (`transition: all`)
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Do / Don't
|
|
129
|
+
|
|
130
|
+
### Do
|
|
131
|
+
- Use one primary button per section of a view *(Material 3, Carbon, Polaris)*
|
|
132
|
+
- Write labels as verb phrases: "Save changes", "Delete account" *(Polaris content guidelines)*
|
|
133
|
+
- Provide `aria-label` for icon-only variants *(WAI-ARIA APG)*
|
|
134
|
+
- Maintain 8px minimum spacing between adjacent buttons *(Carbon, Fluent)*
|
|
135
|
+
|
|
136
|
+
### Don't
|
|
137
|
+
- Don't use a button for navigation to another page — use `<a href>` (Link) *(Carbon, Primer)*
|
|
138
|
+
- Don't disable a button without explaining why — prefer showing error state after submission *(Polaris)*
|
|
139
|
+
- Don't use "Click here" or "Submit" as labels — be specific about the action *(Polaris, Carbon)*
|
|
140
|
+
- Don't truncate button labels — buttons must fit their content *(Fluent, Atlassian)*
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Anti-patterns Cross-links
|
|
145
|
+
|
|
146
|
+
| Anti-pattern | Entry |
|
|
147
|
+
|--------------|-------|
|
|
148
|
+
| BAN-04 | `transition: all` on interactive elements — `reference/anti-patterns.md#ban-04` |
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Benchmark Citations
|
|
153
|
+
|
|
154
|
+
| Claim | Sources |
|
|
155
|
+
|-------|---------|
|
|
156
|
+
| 40px default height | Carbon, Polaris, Fluent 2 |
|
|
157
|
+
| 96% press scale | Material 3, shadcn, Carbon |
|
|
158
|
+
| One primary per section | Material 3, Carbon, Polaris, Fluent |
|
|
159
|
+
| Space/Enter activation | WAI-ARIA APG §4.2 |
|
|
160
|
+
| 38% opacity disabled | Material 3 |
|
|
161
|
+
|
|
162
|
+
Full system URLs: `connections/design-corpora.md`
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Grep Signatures
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# Icon-only button missing aria-label
|
|
170
|
+
grep -rn '<button' src/ | grep -v 'aria-label\|aria-labelledby' | grep 'icon\|svg'
|
|
171
|
+
|
|
172
|
+
# div/span used as button without role
|
|
173
|
+
grep -rn '<div\|<span' src/ | grep 'onClick\|on:click' | grep -v 'role="button"'
|
|
174
|
+
|
|
175
|
+
# transition: all on button (BAN-04)
|
|
176
|
+
grep -rn 'transition:\s*all' src/ | grep -i 'button\|btn'
|
|
177
|
+
|
|
178
|
+
# Missing focus-visible — outline: none without alternative
|
|
179
|
+
grep -rn 'outline:\s*none\|outline:\s*0' src/ | grep -i 'button\|btn\|focus'
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Failing Example
|
|
185
|
+
|
|
186
|
+
```html
|
|
187
|
+
<!-- BAD: div used as button — no keyboard access, no role, no focus management -->
|
|
188
|
+
<div class="btn" onclick="handleClick()">
|
|
189
|
+
<svg><!-- icon --></svg>
|
|
190
|
+
</div>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Why it fails**: Not reachable by keyboard; no ARIA role; Space/Enter do nothing; no accessible name.
|
|
194
|
+
**Grep detection**: `grep -rn '<div.*onClick\|<div.*on:click' src/ | grep -v 'role='`
|
|
195
|
+
**Fix**: Use `<button type="button" aria-label="[action]">` with the icon inside.
|