@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.
- package/.claude-plugin/marketplace.json +9 -5
- package/.claude-plugin/plugin.json +19 -5
- package/CHANGELOG.md +122 -0
- package/README.md +41 -0
- package/SKILL.md +4 -1
- package/agents/component-benchmark-harvester.md +112 -0
- package/agents/component-benchmark-synthesizer.md +88 -0
- package/agents/design-auditor.md +60 -1
- package/agents/design-doc-writer.md +21 -0
- package/agents/design-executor.md +22 -4
- package/agents/design-pattern-mapper.md +61 -0
- package/agents/motion-mapper.md +74 -9
- package/agents/token-mapper.md +8 -0
- package/connections/design-corpora.md +158 -0
- package/package.json +13 -3
- package/reference/components/README.md +94 -0
- package/reference/components/TEMPLATE.md +184 -0
- package/reference/components/accordion.md +217 -0
- package/reference/components/alert.md +198 -0
- package/reference/components/badge.md +202 -0
- package/reference/components/breadcrumbs.md +198 -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/chip.md +209 -0
- package/reference/components/command-palette.md +228 -0
- package/reference/components/date-picker.md +227 -0
- package/reference/components/drawer.md +201 -0
- package/reference/components/file-upload.md +219 -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/list.md +217 -0
- package/reference/components/menu.md +212 -0
- package/reference/components/modal-dialog.md +210 -0
- package/reference/components/navbar.md +211 -0
- package/reference/components/pagination.md +205 -0
- package/reference/components/popover.md +197 -0
- package/reference/components/progress.md +210 -0
- package/reference/components/radio.md +203 -0
- package/reference/components/rich-text-editor.md +226 -0
- package/reference/components/select-combobox.md +219 -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/switch.md +194 -0
- package/reference/components/table.md +229 -0
- package/reference/components/tabs.md +213 -0
- package/reference/components/toast.md +200 -0
- package/reference/components/tooltip.md +201 -0
- package/reference/components/tree.md +225 -0
- package/reference/css-grid-layout.md +835 -0
- package/reference/external/NOTICE.hyperframes +28 -0
- package/reference/image-optimization.md +582 -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/output-contracts/motion-map.schema.json +135 -0
- package/reference/registry.json +285 -0
- package/reference/registry.schema.json +6 -1
- 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
- package/skills/benchmark/SKILL.md +105 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Card — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Material 3, Polaris, Carbon, Atlassian, Mantine, shadcn/ui, Ant Design, Fluent 2
|
|
4
|
+
**Wave**: 2 · **Category**: Containers
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
A card is a contained surface that groups related information and actions. It is a visual container, not a navigation element by default — only make the entire card clickable when the primary action is navigation and there is a single dominant action. Mixed-content cards with multiple actions should not be entirely clickable. *(Material 3, Polaris, Carbon all agree on this boundary)*
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Anatomy
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
┌────────────────────────────┐ ← card surface (border/shadow/background)
|
|
18
|
+
│ [Media / Image] │ ← optional, always with alt text
|
|
19
|
+
│────────────────────────────│
|
|
20
|
+
│ Eyebrow / Category │ ← optional; 12px/600 uppercase
|
|
21
|
+
│ Title │ ← primary label; ≥16px
|
|
22
|
+
│ Description │ ← supporting text; 14px/400
|
|
23
|
+
│────────────────────────────│
|
|
24
|
+
│ [Action 1] [Action 2] │ ← optional action area
|
|
25
|
+
└────────────────────────────┘
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Part | Required | Notes |
|
|
29
|
+
|------|----------|-------|
|
|
30
|
+
| Container | Yes | `<article>` for standalone content; `<div>` for layout |
|
|
31
|
+
| Title | Yes | Descriptive; h2/h3 depending on hierarchy |
|
|
32
|
+
| Content | Yes | Body text, metadata, or media |
|
|
33
|
+
| Media / image | No | Always provide `alt`; decorative images use `alt=""` |
|
|
34
|
+
| Actions | No | Keep ≤2 primary actions per card |
|
|
35
|
+
| Footer / metadata | No | 12px/400; secondary information |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Variants
|
|
40
|
+
|
|
41
|
+
| Variant | Description | Systems |
|
|
42
|
+
|---------|-------------|---------|
|
|
43
|
+
| Elevated | Drop shadow; floats above surface | Material 3, shadcn |
|
|
44
|
+
| Outlined | Border, no shadow | Material 3, Carbon, Polaris |
|
|
45
|
+
| Filled | Filled background, no shadow or border | Material 3, Mantine |
|
|
46
|
+
| Clickable / Interactive | Entire card is a link or button | Material 3, Polaris, Carbon |
|
|
47
|
+
| Horizontal | Media left, content right | Carbon, Polaris, Atlassian |
|
|
48
|
+
| Compact | Dense layout; no media | Carbon, Fluent |
|
|
49
|
+
|
|
50
|
+
**Norm** (≥5/18): outlined or elevated; ≤2 actions per card.
|
|
51
|
+
**Diverge**: elevation vs. outline — both are valid; use elevated for content that needs to float (dashboards), outlined for dense lists (tables of cards).
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## States
|
|
56
|
+
|
|
57
|
+
| State | Trigger | Visual | ARIA/HTML |
|
|
58
|
+
|-------|---------|--------|-----------|
|
|
59
|
+
| default | — | Resting surface | — |
|
|
60
|
+
| hover (clickable) | pointer | Shadow deepens or border darkens | — |
|
|
61
|
+
| focus (clickable) | keyboard | 2px focus ring on outer card | — |
|
|
62
|
+
| active (clickable) | mousedown | Scale 0.99 | — |
|
|
63
|
+
| selected | programmatic | Border + background tint | `aria-selected="true"` |
|
|
64
|
+
| loading | async content | Skeleton placeholder | `aria-busy="true"` |
|
|
65
|
+
| disabled | programmatic | 38% opacity | `aria-disabled="true"` |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Sizing & Spacing
|
|
70
|
+
|
|
71
|
+
| Property | Value | Notes |
|
|
72
|
+
|----------|-------|-------|
|
|
73
|
+
| Padding | 16px (sm), 20px (md), 24px (lg) | *(Carbon: 16px default, Material 3: 16px)* |
|
|
74
|
+
| Border radius | Use token; match `reference/surfaces.md` concentric rule | |
|
|
75
|
+
| Media aspect ratio | 16:9 (landscape), 1:1 (square) | `object-fit: cover` |
|
|
76
|
+
| Min width | 240px | Prevents content collapse |
|
|
77
|
+
| Max width | 480px (typical) | Grid controls actual width; max-width is a guideline |
|
|
78
|
+
| Gap between cards | 16px (sm grid), 24px (md grid) | |
|
|
79
|
+
|
|
80
|
+
Cross-link: `reference/surfaces.md` — concentric radius, 3-layer shadow formula, elevation tokens
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Typography
|
|
85
|
+
|
|
86
|
+
- Title: 16–20px/600 depending on card prominence (h2/h3 in DOM hierarchy)
|
|
87
|
+
- Eyebrow: 11px/600 uppercase, muted colour
|
|
88
|
+
- Body: 14px/400
|
|
89
|
+
- Metadata: 12px/400 muted
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Keyboard & Accessibility
|
|
94
|
+
|
|
95
|
+
> **WAI-ARIA role**: No specific card role. Use `<article>` for independent pieces of content; `<li>` in a list of cards; `<div>` for layout grouping.
|
|
96
|
+
|
|
97
|
+
### Clickable Card Rules
|
|
98
|
+
|
|
99
|
+
*Per WAI-ARIA APG link + button patterns — W3C — 2024*
|
|
100
|
+
|
|
101
|
+
| Invocation | Element | Key |
|
|
102
|
+
|------------|---------|-----|
|
|
103
|
+
| Navigate to new page | `<a href>` wrapping or inside card | Enter |
|
|
104
|
+
| Trigger action in context | `<button>` wrapping or inside card | Enter, Space |
|
|
105
|
+
|
|
106
|
+
- **Do not wrap an entire card in `<a>` if it contains other interactive elements** (links, buttons inside) — nested interactive elements are inaccessible by keyboard
|
|
107
|
+
- Use the "card with primary action + secondary actions" pattern: one `<a>` stretched via `::after` pseudo-element to fill the card; secondary action buttons sit above in stacking context
|
|
108
|
+
|
|
109
|
+
### Accessibility Rules
|
|
110
|
+
|
|
111
|
+
- Card with image: always provide `alt`; use `alt=""` for decorative images
|
|
112
|
+
- Clickable card: heading inside card should be the accessible name (via `aria-labelledby` or the stretched-link pattern)
|
|
113
|
+
- Card grid: use `role="list"` on the grid container and `role="listitem"` on each card, or a semantic `<ul>/<li>` structure, so screen readers announce item count
|
|
114
|
+
- Loading skeleton: add `aria-busy="true"` on the card container; remove when content loads
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Motion
|
|
119
|
+
|
|
120
|
+
| Transition | Duration | Easing | Notes |
|
|
121
|
+
|------------|----------|--------|-------|
|
|
122
|
+
| hover shadow | 150ms | ease-out | Elevation increase |
|
|
123
|
+
| press scale | 80ms | ease | 1→0.99 (subtle; card is large) |
|
|
124
|
+
| skeleton shimmer | 1.5s | linear loop | Respect `prefers-reduced-motion` |
|
|
125
|
+
|
|
126
|
+
Cross-link: `reference/motion.md` — Skeleton shimmer pattern, `prefers-reduced-motion`
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Do / Don't
|
|
131
|
+
|
|
132
|
+
### Do
|
|
133
|
+
- Use `<article>` when the card is an independent, self-contained piece of content *(Carbon, Polaris)*
|
|
134
|
+
- Keep clickable cards to a single primary action; surface secondary actions as explicit buttons *(Material 3, Polaris)*
|
|
135
|
+
- Use the stretched-link (`::after`) pattern for clickable cards with nested links/buttons *(Carbon, Bootstrap pattern)*
|
|
136
|
+
- Provide `alt` text for all card images, or `alt=""` for decorative images *(WCAG 1.1.1)*
|
|
137
|
+
|
|
138
|
+
### Don't
|
|
139
|
+
- Don't wrap the entire card in `<a>` if it contains other interactive elements *(WAI-ARIA APG)*
|
|
140
|
+
- Don't use `<div>` as a clickable card without `role="button"` or `role="link"` + keyboard handler *(WAI-ARIA APG)*
|
|
141
|
+
- Don't place the entire card title in a plain `<span>` when it could be `<h2>/<h3>` *(Atlassian, Carbon)*
|
|
142
|
+
- Don't use more than 2 primary actions per card — extract to a detail view *(Material 3, Polaris)*
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Anti-patterns Cross-links
|
|
147
|
+
|
|
148
|
+
| Anti-pattern | Entry |
|
|
149
|
+
|--------------|-------|
|
|
150
|
+
| Nested interactive elements in clickable container | `reference/anti-patterns.md` |
|
|
151
|
+
| Missing alt on card media | `reference/anti-patterns.md` |
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Benchmark Citations
|
|
156
|
+
|
|
157
|
+
| Claim | Sources |
|
|
158
|
+
|-------|---------|
|
|
159
|
+
| ≤2 actions per card | Material 3, Polaris, Carbon |
|
|
160
|
+
| Stretched-link pattern for nested interactivity | Carbon, Bootstrap |
|
|
161
|
+
| article element for standalone card content | Carbon, Polaris |
|
|
162
|
+
| 16px default padding | Carbon, Material 3 |
|
|
163
|
+
|
|
164
|
+
Full system URLs: `connections/design-corpora.md`
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Grep Signatures
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# Entire card wrapped in <a> with nested buttons/links
|
|
172
|
+
grep -rn '<a ' src/ | grep -i 'card' | xargs grep -l 'button\|<a ' 2>/dev/null
|
|
173
|
+
|
|
174
|
+
# Clickable div without role
|
|
175
|
+
grep -rn 'class.*card\|data-testid.*card' src/ | grep 'onClick\|on:click' | grep -v 'role='
|
|
176
|
+
|
|
177
|
+
# Card image without alt
|
|
178
|
+
grep -rn '<img' src/ | grep -i 'card' | grep -v 'alt='
|
|
179
|
+
|
|
180
|
+
# Missing heading hierarchy in card
|
|
181
|
+
grep -rn 'class.*card' src/ | xargs grep -L 'h[1-6]\|aria-label' 2>/dev/null
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Failing Example
|
|
187
|
+
|
|
188
|
+
```html
|
|
189
|
+
<!-- BAD: entire card is <a> but contains a button — button is inaccessible by keyboard in most AT -->
|
|
190
|
+
<a href="/product/42" class="card">
|
|
191
|
+
<img src="product.jpg" />
|
|
192
|
+
<h3>Product Name</h3>
|
|
193
|
+
<p>Description…</p>
|
|
194
|
+
<button onclick="addToCart(42)">Add to cart</button>
|
|
195
|
+
</a>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Why it fails**: Nested `<button>` inside `<a>` is invalid HTML. Keyboard users pressing Tab inside the link may reach the button, but Enter on the button may also trigger the link. Screen reader behavior is unpredictable.
|
|
199
|
+
**Grep detection**: `grep -rn '<a.*href' src/ | xargs grep -l '<button' 2>/dev/null`
|
|
200
|
+
**Fix**: Use the stretched-link pattern — `<h3><a href="/product/42">Product Name</a></h3>` with `::after { position: absolute; inset: 0; }` on the `<a>`, and position the "Add to cart" button above via `position: relative; z-index: 1`.
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Checkbox — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Material 3, Carbon, Polaris, Ant Design, WAI-ARIA APG, Mantine, Chakra UI, Atlassian
|
|
4
|
+
**Wave**: 1 · **Category**: Inputs
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
A checkbox allows the user to select or deselect a binary option, or to represent an indeterminate state (partially selected group). Checkboxes are independent — selecting one does not affect others. Use radio buttons for mutually exclusive choices within a group. Always group related checkboxes in a `<fieldset>` with a `<legend>`.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Anatomy
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
┌──┐ Label text
|
|
18
|
+
│✓ │ ← <input type="checkbox" id="x"> (or role="checkbox")
|
|
19
|
+
└──┘ Helper text (opt.)
|
|
20
|
+
Error message (opt.)
|
|
21
|
+
|
|
22
|
+
Group:
|
|
23
|
+
<fieldset>
|
|
24
|
+
<legend>Preferences</legend>
|
|
25
|
+
[checkbox] Option A
|
|
26
|
+
[checkbox] Option B
|
|
27
|
+
[checkbox] Option C (indeterminate)
|
|
28
|
+
</fieldset>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Part | Required | Notes |
|
|
32
|
+
|------|----------|-------|
|
|
33
|
+
| Input / control | Yes | Native `<input type="checkbox">` preferred |
|
|
34
|
+
| Label | Yes | `<label for="id">` — click zone includes label text |
|
|
35
|
+
| Fieldset + legend | Yes (group) | Required when ≥2 related checkboxes |
|
|
36
|
+
| Helper text | No | Below label; `aria-describedby` |
|
|
37
|
+
| Error message | Conditional | Field-level or group-level |
|
|
38
|
+
| Indeterminate indicator | Conditional | Dash/minus mark; set via `.indeterminate = true` (JS) |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Variants
|
|
43
|
+
|
|
44
|
+
| Variant | Description | Systems |
|
|
45
|
+
|---------|-------------|---------|
|
|
46
|
+
| Default | Binary checked / unchecked | All |
|
|
47
|
+
| Indeterminate | Partial selection indicator (parent of group) | Material 3, Carbon, Ant, Mantine |
|
|
48
|
+
| Standalone | Single checkbox (e.g. "I agree to terms") | All |
|
|
49
|
+
| Group | ≥2 checkboxes in `<fieldset>` | All |
|
|
50
|
+
| With description | Label + helper text below | Material 3, Polaris, Carbon |
|
|
51
|
+
|
|
52
|
+
**Norm** (≥6/18): indeterminate state is a visual-only UI state — the underlying `checked` value is still boolean; set via DOM `.indeterminate` property, not HTML attribute.
|
|
53
|
+
**Diverge**: size — Material 3 uses 18px; Carbon 16px; Polaris 16px; Ant 14–16px. 16px is the de-facto norm.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## States
|
|
58
|
+
|
|
59
|
+
| State | Trigger | Visual | ARIA |
|
|
60
|
+
|-------|---------|--------|------|
|
|
61
|
+
| unchecked | default | Empty box | `aria-checked="false"` |
|
|
62
|
+
| checked | user interaction | Checkmark | `aria-checked="true"` |
|
|
63
|
+
| indeterminate | set via JS | Dash / minus | `aria-checked="mixed"` |
|
|
64
|
+
| hover | pointer over | Box border darkens | — |
|
|
65
|
+
| focus | keyboard | 2px focus ring around box | — |
|
|
66
|
+
| disabled unchecked | `disabled` | 38% opacity | `aria-disabled="true"` |
|
|
67
|
+
| disabled checked | `disabled` | 38% opacity + check | `aria-disabled="true"` |
|
|
68
|
+
| error | validation | Red box border | `aria-invalid="true"` |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Sizing & Spacing
|
|
73
|
+
|
|
74
|
+
| Property | Value | Notes |
|
|
75
|
+
|----------|-------|-------|
|
|
76
|
+
| Control size | 16×16px (20px touch target via pseudo-element) | *(Carbon, Polaris, Material 3)* |
|
|
77
|
+
| Gap: control → label | 8px | |
|
|
78
|
+
| Label min click zone | Full row width | Increases tap target |
|
|
79
|
+
| Group item spacing | 8px vertical between items | *(Carbon, Material 3)* |
|
|
80
|
+
| Indentation (nested) | 24px | When showing hierarchical groups |
|
|
81
|
+
|
|
82
|
+
Cross-link: `reference/surfaces.md` — hit-area pseudo-element pattern (44×44px minimum touch target)
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Typography
|
|
87
|
+
|
|
88
|
+
- Label: 14px/400; same weight as body — checkboxes are options, not headings
|
|
89
|
+
- Legend: 14px/500 or 12px/600 uppercase — distinguishes group from items
|
|
90
|
+
- Helper/error: 12px/400
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Keyboard & Accessibility
|
|
95
|
+
|
|
96
|
+
> **WAI-ARIA role**: `checkbox` (implicit on `<input type="checkbox">`)
|
|
97
|
+
> **Required attributes**: `aria-checked` (if not native); `aria-describedby` for helper/error
|
|
98
|
+
|
|
99
|
+
### Keyboard Contract
|
|
100
|
+
|
|
101
|
+
*Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/ — W3C — 2024*
|
|
102
|
+
|
|
103
|
+
| Key | Action |
|
|
104
|
+
|-----|--------|
|
|
105
|
+
| Tab | Moves focus to the checkbox |
|
|
106
|
+
| Space | Toggles the checkbox state (checked / unchecked / indeterminate) |
|
|
107
|
+
|
|
108
|
+
Within a group: Tab moves between checkboxes (not arrow keys — checkboxes are independent).
|
|
109
|
+
|
|
110
|
+
### Accessibility Rules
|
|
111
|
+
|
|
112
|
+
- Label MUST be associated via `<label for="id">` — clicking the label must toggle the checkbox
|
|
113
|
+
- `<fieldset>` + `<legend>` MUST wrap every group of related checkboxes — the legend provides group context to screen readers
|
|
114
|
+
- Indeterminate state MUST be set via JS `.indeterminate = true` — there is no HTML attribute; `aria-checked="mixed"` must be set simultaneously on `role="checkbox"` elements
|
|
115
|
+
- Disabled checkboxes: use native `disabled` attribute for form semantics; `aria-disabled="true"` if the element must remain in tab order (e.g. with explanatory tooltip)
|
|
116
|
+
- Error state: `aria-invalid="true"` on the control; group-level error on the `<fieldset>` via `aria-describedby`
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Motion
|
|
121
|
+
|
|
122
|
+
| Transition | Duration | Easing | Notes |
|
|
123
|
+
|------------|----------|--------|-------|
|
|
124
|
+
| check fill | 120ms | ease-out | SVG path draw or scale from center |
|
|
125
|
+
| indeterminate dash | 120ms | ease | Width animation of dash element |
|
|
126
|
+
| hover border | 80ms | ease | Border colour only |
|
|
127
|
+
|
|
128
|
+
Cross-link: `reference/motion.md` — `prefers-reduced-motion`: skip path animation, show fill instantly
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Do / Don't
|
|
133
|
+
|
|
134
|
+
### Do
|
|
135
|
+
- Use `<fieldset>` + `<legend>` for every group *(WAI-ARIA APG, Carbon, Polaris)*
|
|
136
|
+
- Set indeterminate via `.indeterminate = true` AND `aria-checked="mixed"` *(WAI-ARIA APG, Mantine)*
|
|
137
|
+
- Make the entire label row clickable, not just the box *(Material 3, Carbon, Polaris)*
|
|
138
|
+
- Align label text to the top of the control in multiline label scenarios *(Carbon)*
|
|
139
|
+
|
|
140
|
+
### Don't
|
|
141
|
+
- Don't use checkboxes for mutually exclusive options — use radio buttons *(Material 3, Carbon, Polaris)*
|
|
142
|
+
- Don't use a custom `<div>` checkbox without `role="checkbox"` and keyboard handler *(WAI-ARIA APG)*
|
|
143
|
+
- Don't set `aria-checked="mixed"` via HTML attribute — it must be set dynamically *(WAI-ARIA APG)*
|
|
144
|
+
- Don't rely on colour alone for checked state — always include a visible checkmark *(WCAG 1.4.1)*
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Anti-patterns Cross-links
|
|
149
|
+
|
|
150
|
+
| Anti-pattern | Entry |
|
|
151
|
+
|--------------|-------|
|
|
152
|
+
| Custom checkbox without role | `reference/anti-patterns.md` |
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Benchmark Citations
|
|
157
|
+
|
|
158
|
+
| Claim | Sources |
|
|
159
|
+
|-------|---------|
|
|
160
|
+
| Space toggles checkbox | WAI-ARIA APG §3.5 |
|
|
161
|
+
| fieldset+legend required for group | WAI-ARIA APG, Carbon, Polaris |
|
|
162
|
+
| .indeterminate = true (JS only) | WAI-ARIA APG, MDN |
|
|
163
|
+
| 16px control size | Carbon, Polaris, Material 3 |
|
|
164
|
+
|
|
165
|
+
Full system URLs: `connections/design-corpora.md`
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Grep Signatures
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# Custom checkbox div/span without role="checkbox"
|
|
173
|
+
grep -rn 'class.*checkbox\|type.*checkbox' src/ | grep '<div\|<span' | grep -v 'role='
|
|
174
|
+
|
|
175
|
+
# Missing label association
|
|
176
|
+
grep -rn 'type="checkbox"' src/ | grep -v 'id=\|aria-label'
|
|
177
|
+
|
|
178
|
+
# Group without fieldset
|
|
179
|
+
grep -rn 'checkbox' src/ | grep -v 'fieldset\|role="group"'
|
|
180
|
+
|
|
181
|
+
# Indeterminate set via attribute instead of JS
|
|
182
|
+
grep -rn 'indeterminate' src/ | grep 'setAttribute\|attr(' | grep '"true"'
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Failing Example
|
|
188
|
+
|
|
189
|
+
```html
|
|
190
|
+
<!-- BAD: checkboxes in a group without fieldset/legend — group context lost for screen readers -->
|
|
191
|
+
<div>
|
|
192
|
+
<p>Notification preferences</p>
|
|
193
|
+
<input type="checkbox" id="email"> <label for="email">Email</label>
|
|
194
|
+
<input type="checkbox" id="sms"> <label for="sms">SMS</label>
|
|
195
|
+
</div>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Why it fails**: Screen readers announce each option without the group context ("Email — checkbox") — users don't know what these options belong to without reading surrounding text.
|
|
199
|
+
**Grep detection**: `grep -B5 'type="checkbox"' src/ | grep -v 'fieldset\|role="group"'`
|
|
200
|
+
**Fix**:
|
|
201
|
+
```html
|
|
202
|
+
<fieldset>
|
|
203
|
+
<legend>Notification preferences</legend>
|
|
204
|
+
<input type="checkbox" id="email"> <label for="email">Email</label>
|
|
205
|
+
<input type="checkbox" id="sms"> <label for="sms">SMS</label>
|
|
206
|
+
</fieldset>
|
|
207
|
+
```
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Chip / Tag — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Material 3, Carbon, Atlassian, Mantine
|
|
4
|
+
**Wave**: 3 · **Category**: Feedback
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
A chip (or tag) is a compact, interactive label that represents an attribute, filter, or selection. It is commonly used for multi-select filter interfaces, tag-input fields (user-generated labels), one-time suggestion prompts, and static display labels. Each variant has distinct interaction semantics — filter chips toggle on/off, input chips are removable, suggestion chips trigger a one-time action, and display chips are static. *(Material 3, Carbon, Atlassian, Mantine agree on the four-variant taxonomy)*
|
|
11
|
+
|
|
12
|
+
*UUPM app-interface: filter chips in search/filter UIs and tag input patterns (MIT attribution)*
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Anatomy
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
┌───────────────────────────────┐
|
|
20
|
+
│ [icon?] Label text [✕ remove?] │
|
|
21
|
+
└───────────────────────────────┘
|
|
22
|
+
↑ role varies by variant
|
|
23
|
+
↑ touch target ≥ 32px height
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
| Part | Required | Notes |
|
|
27
|
+
|------|----------|-------|
|
|
28
|
+
| Label text | Yes | Concise (1–3 words typical) |
|
|
29
|
+
| Container | Yes | Interactive element; role/type depends on variant |
|
|
30
|
+
| Leading icon | No | 16px; left-aligned; 8px gap to label |
|
|
31
|
+
| Remove button | Conditional | Required on input/tag chips; independent focusable element |
|
|
32
|
+
| Selected state indicator | Conditional | Checkmark or filled style on toggled filter chips |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Variants
|
|
37
|
+
|
|
38
|
+
| Variant | Interaction | Role | Systems |
|
|
39
|
+
|---------|-------------|------|---------|
|
|
40
|
+
| Filter | Toggleable; multi-select | `aria-pressed` or `role="option"` in `role="listbox"` | Material 3, Carbon, Atlassian, Mantine |
|
|
41
|
+
| Input / Tag | User-generated; removable via × button | `role="option"` in combobox; or `role="listbox"` child | Material 3, Atlassian, Mantine |
|
|
42
|
+
| Suggestion | One-time select; fires an action then typically disappears | `role="button"` | Material 3, Mantine |
|
|
43
|
+
| Display | Static label; no interaction | No role needed (or `role="listitem"`) | Atlassian (Lozenge), Carbon (Tag), Mantine |
|
|
44
|
+
|
|
45
|
+
**Norm** (≥4/18 systems agree): filter chip uses `aria-pressed` for standalone toggles; input chip requires independent remove button with its own `aria-label`; display chip is decorative/static.
|
|
46
|
+
**Diverge**: Material 3 calls static chips "Suggestion chips" when one-time-action and "Assist chips" for shortcut actions; Atlassian separates "Tag" (removable) from "Lozenge" (static status label); Carbon calls them "Tag" uniformly with a `filter` prop.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## States
|
|
51
|
+
|
|
52
|
+
| State | Trigger | Visual | ARIA |
|
|
53
|
+
|-------|---------|--------|------|
|
|
54
|
+
| default | — | Rest fill + label | — |
|
|
55
|
+
| hover | pointer over chip | Background tint (+8%) | — |
|
|
56
|
+
| focus | keyboard focus on chip | focus-visible ring (2px) | — |
|
|
57
|
+
| selected (filter) | click / Enter / Space | Filled background; checkmark icon | `aria-pressed="true"` |
|
|
58
|
+
| unselected (filter) | click / Enter / Space | Outline style | `aria-pressed="false"` |
|
|
59
|
+
| disabled | `disabled` / `aria-disabled` | 38% opacity; cursor not-allowed | `aria-disabled="true"` |
|
|
60
|
+
| remove focus | Tab to × button | Focus ring on × | — |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Sizing & Spacing
|
|
65
|
+
|
|
66
|
+
| Property | Value | Notes |
|
|
67
|
+
|----------|-------|-------|
|
|
68
|
+
| Height | 32px (min touch target) | Do not reduce below 32px |
|
|
69
|
+
| Padding-x | 12px | Each side; 8px when leading icon present |
|
|
70
|
+
| Gap (icon → label) | 8px | |
|
|
71
|
+
| Gap (label → remove ×) | 4px | |
|
|
72
|
+
| Remove button size | 20×20px (visual); 32×32px (touch) | Extend touch area with padding |
|
|
73
|
+
| Border radius | 16–20px (full pill) | *(Material 3: full-radius pill; Carbon: 4px; Atlassian: 2px)* |
|
|
74
|
+
| Font size | 13–14px / 400 | |
|
|
75
|
+
|
|
76
|
+
**Norm**: pill shape (full-radius) for filter and input chips *(Material 3, Mantine)*; Touch target ≥ 32px height *(WCAG 2.5.5, Material 3)*.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Typography
|
|
81
|
+
|
|
82
|
+
- Label: body-sm (13–14px/400) — same scale as form labels
|
|
83
|
+
- No bold weight — chip label is a tag, not a heading or CTA
|
|
84
|
+
- Truncate with ellipsis only when chip is in a fixed-width container; prefer wrapping chip set to natural width
|
|
85
|
+
|
|
86
|
+
Cross-link: `reference/typography.md` — body-sm definition
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Keyboard & Accessibility
|
|
91
|
+
|
|
92
|
+
> **WAI-ARIA role**: `button` (filter standalone, suggestion); `option` inside `listbox` (filter group, input chips)
|
|
93
|
+
> **Required attributes**: `aria-pressed` on standalone filter chip; `aria-label` on remove button; `aria-selected` if `role="option"`
|
|
94
|
+
|
|
95
|
+
### Keyboard Contract
|
|
96
|
+
|
|
97
|
+
*Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/listbox/ — W3C — 2024*
|
|
98
|
+
|
|
99
|
+
| Key | Action |
|
|
100
|
+
|-----|--------|
|
|
101
|
+
| Tab | Moves focus to the chip or to the remove (×) button |
|
|
102
|
+
| Enter / Space | Toggles filter chip (pressed/unpressed); activates suggestion chip |
|
|
103
|
+
| Delete / Backspace | Removes an input/tag chip (when chip or its remove button has focus) |
|
|
104
|
+
| Arrow Left / Right | Navigates between chips when inside a `role="listbox"` group |
|
|
105
|
+
| Escape | Collapses chip group if it was expanded (e.g. overflow +N pattern) |
|
|
106
|
+
|
|
107
|
+
### Accessibility Rules
|
|
108
|
+
|
|
109
|
+
- Filter chip used as standalone toggle MUST have `aria-pressed="true|false"` — absence means screen readers cannot report toggle state *(WAI-ARIA APG)*
|
|
110
|
+
- Filter chips inside a multi-select group SHOULD use `role="option"` inside `role="listbox"` with `aria-multiselectable="true"` *(WAI-ARIA APG)*
|
|
111
|
+
- Remove button MUST have an independent `aria-label` describing what it removes: `aria-label="Remove Python tag"` — the × glyph alone is not an accessible name *(WAI-ARIA APG, Material 3)*
|
|
112
|
+
- Remove button MUST be a separate focusable element, NOT a click handler on the chip label *(Carbon, Atlassian)*
|
|
113
|
+
- Touch target for remove button MUST be ≥ 32×32px via padding even if the visual × is 16–20px *(WCAG 2.5.5)*
|
|
114
|
+
- Display chips have no role — they are presentational; if they carry meaningful status, use `role="status"` or integrate into an Alert *(Atlassian Lozenge guidance)*
|
|
115
|
+
|
|
116
|
+
Cross-link: `reference/accessibility.md` — `aria-pressed`, `listbox`, touch targets
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Motion
|
|
121
|
+
|
|
122
|
+
| Transition | Duration | Easing | Notes |
|
|
123
|
+
|------------|----------|--------|-------|
|
|
124
|
+
| Toggle selected (background fill) | 100ms | ease-out | Background + checkmark appear |
|
|
125
|
+
| Chip remove (collapse width) | 150ms | ease-in | Width collapses to 0, siblings shift |
|
|
126
|
+
| Chip add (expand width) | 150ms | ease-out | Width expands from 0 |
|
|
127
|
+
| Hover background | 80ms | ease-out | Subtle tint only |
|
|
128
|
+
|
|
129
|
+
**BAN**: Full-page reflow animation when removing a chip — collapse width inline; do not shift unrelated page sections. Do not use `transition: all` (catches unintended border/shadow changes).
|
|
130
|
+
|
|
131
|
+
Cross-link: `reference/motion.md` — `prefers-reduced-motion`: skip width animation, instant add/remove
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Do / Don't
|
|
136
|
+
|
|
137
|
+
### Do
|
|
138
|
+
- Add `aria-pressed` to standalone filter chips *(WAI-ARIA APG)*
|
|
139
|
+
- Give remove buttons an independent `aria-label`: "Remove [label] tag" *(WAI-ARIA APG, Material 3)*
|
|
140
|
+
- Use `role="listbox"` + `role="option"` for multi-select filter chip groups *(WAI-ARIA APG)*
|
|
141
|
+
- Ensure touch target ≥ 32px height; extend remove button via padding *(WCAG 2.5.5)*
|
|
142
|
+
|
|
143
|
+
### Don't
|
|
144
|
+
- Don't put filter chips without `aria-pressed` — screen readers cannot report selected state *(WAI-ARIA APG)*
|
|
145
|
+
- Don't use the same click handler for chip label and remove — remove must be independently focusable *(Carbon, Atlassian)*
|
|
146
|
+
- Don't truncate chip label in narrow containers without a tooltip — truncated tags lose meaning *(Polaris, Mantine)*
|
|
147
|
+
- Don't use display chips for dynamic statuses that change — use Alert or Badge instead *(Atlassian)*
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Anti-patterns Cross-links
|
|
152
|
+
|
|
153
|
+
| Anti-pattern | Entry |
|
|
154
|
+
|--------------|-------|
|
|
155
|
+
| Filter chip without aria-pressed | `reference/anti-patterns.md#ban-aria-pressed` |
|
|
156
|
+
| Remove button without aria-label | `reference/anti-patterns.md#ban-aria-label` |
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Benchmark Citations
|
|
161
|
+
|
|
162
|
+
| Claim | Sources |
|
|
163
|
+
|-------|---------|
|
|
164
|
+
| aria-pressed on standalone filter chip | WAI-ARIA APG, Material 3, Carbon |
|
|
165
|
+
| Remove button independent aria-label | WAI-ARIA APG, Material 3, Atlassian |
|
|
166
|
+
| role="option" inside role="listbox" for group | WAI-ARIA APG |
|
|
167
|
+
| Touch target ≥ 32px height | WCAG 2.5.5, Material 3 |
|
|
168
|
+
| Delete/Backspace removes input chip | Material 3, Atlassian, Mantine |
|
|
169
|
+
| UUPM filter/tag-input patterns | UUPM app-interface (MIT) |
|
|
170
|
+
|
|
171
|
+
Full system URLs: `connections/design-corpora.md`
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Grep Signatures
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# Filter chip missing aria-pressed
|
|
179
|
+
grep -rn 'chip\|Chip\|filter-tag\|filterChip' src/ | grep -i 'filter\|toggle' | grep -v 'aria-pressed'
|
|
180
|
+
|
|
181
|
+
# Remove button on chip missing aria-label
|
|
182
|
+
grep -rn 'chip.*remove\|tag.*remove\|remove.*chip\|remove.*tag' src/ | grep -v 'aria-label'
|
|
183
|
+
|
|
184
|
+
# Chip remove button without independent tab stop (not a separate button element)
|
|
185
|
+
grep -rn 'chip\|tag' src/ | grep '×\|×\|✕\|close' | grep -v '<button\|role="button"'
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Failing Example
|
|
191
|
+
|
|
192
|
+
```html
|
|
193
|
+
<!-- BAD: filter chip with no aria-pressed — screen readers cannot report toggle state -->
|
|
194
|
+
<div class="chip chip--filter" onclick="toggleFilter(this)">
|
|
195
|
+
Python
|
|
196
|
+
</div>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Why it fails**: `<div>` is not keyboard reachable. No `aria-pressed` means screen readers cannot determine if the filter is active or inactive. No `role="button"` or `tabindex` so keyboard users skip it entirely. No focus-visible ring.
|
|
200
|
+
**Grep detection**: `grep -rn 'chip--filter\|filterChip\|chip.*filter' src/ | grep -v 'aria-pressed'`
|
|
201
|
+
**Fix**:
|
|
202
|
+
```html
|
|
203
|
+
<button class="chip chip--filter"
|
|
204
|
+
aria-pressed="false"
|
|
205
|
+
onclick="toggleFilter(this)">
|
|
206
|
+
Python
|
|
207
|
+
</button>
|
|
208
|
+
```
|
|
209
|
+
Update `aria-pressed` to `"true"` when selected.
|