@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,219 @@
|
|
|
1
|
+
# File Upload — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Polaris (DropZone), Carbon (FileUploader), Atlassian Design System, Material 3
|
|
4
|
+
**Wave**: 5 · **Category**: Advanced
|
|
5
|
+
**Spec file**: `reference/components/file-upload.md`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
A File Upload component lets users attach one or more files by dragging them onto a drop zone or clicking to open the native file picker. It must work for all users: keyboard-only users activate the hidden-but-accessible `<input type="file">`, while pointer users can drag-drop. A file list tracks upload progress, status, and provides remove actions. *(Polaris, Carbon, Atlassian agree: drop zone + accessible file input + per-file status list is the canonical pattern.)*
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Anatomy
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
┌─────────────────────────────────┐
|
|
19
|
+
│ Drop zone │
|
|
20
|
+
│ [ Cloud icon ] │
|
|
21
|
+
│ "Drag files here or" │
|
|
22
|
+
│ [ Browse files ] ← triggers │
|
|
23
|
+
│ <input type="file"> │
|
|
24
|
+
└─────────────────────────────────┘
|
|
25
|
+
|
|
26
|
+
File list (appears after selection):
|
|
27
|
+
┌────────────────────────────────────────────────┐
|
|
28
|
+
│ 📄 report.pdf 245 KB [======== ] 80% [✕] │
|
|
29
|
+
│ 📄 photo.jpg 1.2 MB ✓ Done [✕] │
|
|
30
|
+
│ 📄 data.csv 88 KB ✗ Error [✕] │
|
|
31
|
+
└────────────────────────────────────────────────┘
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| Part | Required | Notes |
|
|
35
|
+
|------|----------|-------|
|
|
36
|
+
| Drop zone container | Yes | Dashed border; drag-over changes fill + border color |
|
|
37
|
+
| `<input type="file">` | Yes | MUST be accessible (not `display:none`) — keyboard fallback |
|
|
38
|
+
| Browse trigger button | Yes | Visually activates the file input; must be a `<button>` or `<label>` |
|
|
39
|
+
| File list | Yes (when files selected) | Per-file name, size, status, progress bar, remove button |
|
|
40
|
+
| Progress bar | Yes (during upload) | `role="progressbar"` + `aria-valuenow` per file or overall |
|
|
41
|
+
| Remove button | Yes | `aria-label="Remove [filename]"` |
|
|
42
|
+
| Error region | Yes (on error) | `aria-live="assertive"` for upload errors |
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Variants
|
|
47
|
+
|
|
48
|
+
| Variant | Description | Systems |
|
|
49
|
+
|---------|-------------|---------|
|
|
50
|
+
| Drop zone | Large dashed-border target area with drag-and-drop | Polaris, Carbon, Atlassian, Material 3 |
|
|
51
|
+
| Compact / inline | Small "Attach file" button only; no large drop area | Carbon (FileUploaderItem), Atlassian |
|
|
52
|
+
| Avatar/image uploader | Circular or rectangular crop zone for single image | Material 3, Polaris |
|
|
53
|
+
| Multi-file | `multiple` attr; list of uploaded files | All systems |
|
|
54
|
+
| Single-file | No `multiple`; replaces previous selection | Carbon, Polaris |
|
|
55
|
+
|
|
56
|
+
**Norm** (≥3/4 systems agree): drop zone + browse button + file list is the standard desktop pattern; progress bar per file during upload.
|
|
57
|
+
**Diverge**: Polaris auto-starts upload on drop; Carbon shows "Add files" button after initial selection to allow adding more; Material 3 defers to app logic.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## States
|
|
62
|
+
|
|
63
|
+
| State | Trigger | Visual | ARIA |
|
|
64
|
+
|-------|---------|--------|------|
|
|
65
|
+
| default | — | Dashed border, instructional text | — |
|
|
66
|
+
| drag-over | File dragged over zone | Filled background + solid border color change | `aria-dropeffect="copy"` (deprecated but still useful) |
|
|
67
|
+
| drag-invalid | Wrong file type dragged over | Error border color; tooltip/message | — |
|
|
68
|
+
| uploading | File being sent | Per-file progress bar animating | `aria-valuenow` on progressbar |
|
|
69
|
+
| upload-done | Transfer complete | Check icon; status text "Done" | — |
|
|
70
|
+
| upload-error | Transfer failed | Error icon; error message per file | `aria-live="assertive"` error region |
|
|
71
|
+
| disabled | `disabled` prop | 38% opacity; drag events ignored | `aria-disabled="true"` on zone |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Sizing & Spacing
|
|
76
|
+
|
|
77
|
+
| Size | Drop Zone Min Height | Border Radius | Font |
|
|
78
|
+
|------|---------------------|---------------|------|
|
|
79
|
+
| sm | 80px | 4px | 13px |
|
|
80
|
+
| md (default) | 128px | 8px | 14px |
|
|
81
|
+
| lg | 200px | 12px | 16px |
|
|
82
|
+
|
|
83
|
+
**Norm**: Drop zone should be large enough that it is comfortably hittable — 128px minimum height for default. File list rows are 48–56px tall for accessible remove-button target size *(Carbon, Polaris)*.
|
|
84
|
+
|
|
85
|
+
Cross-link: `reference/surfaces.md` — minimum 44×44px touch targets for remove buttons.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Typography
|
|
90
|
+
|
|
91
|
+
- Drop zone instruction: body-md, centered, secondary color
|
|
92
|
+
- "Browse files" link/button: body-md, primary link or button style
|
|
93
|
+
- File name in list: body-sm, truncated with ellipsis (max-width on container), full name in `title` attribute
|
|
94
|
+
- File size: caption-sm, secondary color
|
|
95
|
+
- Status text (Done / Error): caption-sm, success or error semantic color
|
|
96
|
+
|
|
97
|
+
Cross-link: `reference/typography.md` — truncation rules.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Keyboard & Accessibility
|
|
102
|
+
|
|
103
|
+
> **WAI-ARIA role**: `button` (browse trigger); `progressbar` (upload progress); `status` or `log` (file list updates)
|
|
104
|
+
> **Required attributes**: `aria-label="Remove [filename]"` on remove buttons; `aria-valuenow` + `aria-valuemin` + `aria-valuemax` on progressbar; `aria-live="assertive"` on error region
|
|
105
|
+
|
|
106
|
+
### Keyboard Contract
|
|
107
|
+
|
|
108
|
+
*Derived from native `<input type="file">` behavior and WAI-ARIA APG button pattern — W3C — 2024*
|
|
109
|
+
|
|
110
|
+
| Key | Action |
|
|
111
|
+
|-----|--------|
|
|
112
|
+
| Tab | Move focus to browse button / file input |
|
|
113
|
+
| Enter / Space | Activate browse button — opens native file picker dialog |
|
|
114
|
+
| Tab (in file list) | Move through file items and remove buttons |
|
|
115
|
+
| Enter / Space (on remove button) | Remove file from list |
|
|
116
|
+
|
|
117
|
+
Drag-and-drop is pointer-only; keyboard users MUST be able to complete the entire task via the file input alone.
|
|
118
|
+
|
|
119
|
+
### Accessibility Rules
|
|
120
|
+
|
|
121
|
+
- `<input type="file">` MUST NOT use `display:none` or `visibility:hidden` — use `opacity:0` positioned absolutely with dimensions matching the trigger, OR keep a visible file input alongside the drop zone
|
|
122
|
+
- The browse trigger MUST be a `<button>` or `<label for="file-input">` so it is keyboard-focusable and activates the input
|
|
123
|
+
- Remove buttons MUST have `aria-label="Remove [filename]"` — an icon-only ✕ with no accessible name fails AT users
|
|
124
|
+
- Upload errors MUST be announced via `aria-live="assertive"` — do not rely solely on visual indicators
|
|
125
|
+
- Progress bars MUST keep `aria-valuenow` updated throughout upload
|
|
126
|
+
- `accept` attribute MUST match the visible allowed-types hint text so users are not surprised by rejection
|
|
127
|
+
- File list additions/removals should be announced via `aria-live="polite"` on the list container
|
|
128
|
+
|
|
129
|
+
Cross-link: `reference/accessibility.md` — aria-live regions, accessible file input patterns.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Motion
|
|
134
|
+
|
|
135
|
+
| Transition | Duration | Easing | Notes |
|
|
136
|
+
|------------|----------|--------|-------|
|
|
137
|
+
| Drop zone drag-over | 100ms | ease-out | Background fill + border color |
|
|
138
|
+
| File list item enter | 200ms | ease-out | Slide-in from top or fade-in |
|
|
139
|
+
| File list item remove | 150ms | ease-in | Fade + collapse height |
|
|
140
|
+
| Progress bar fill | continuous | linear | Matches upload byte progress |
|
|
141
|
+
| Upload complete tick | 200ms | ease-out | Check icon draw animation |
|
|
142
|
+
|
|
143
|
+
**BAN**: Do not animate progress bar with CSS only at a fixed pace — progress MUST reflect actual upload percentage via `aria-valuenow`.
|
|
144
|
+
|
|
145
|
+
Cross-link: `reference/motion.md` — reduced-motion: skip slide/collapse animations; keep progress bar updates.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Do / Don't
|
|
150
|
+
|
|
151
|
+
### Do
|
|
152
|
+
- Keep `<input type="file">` accessible at all times (opacity:0 trick or visible) *(WAI-ARIA, Polaris, Carbon)*
|
|
153
|
+
- Provide `aria-label="Remove [filename]"` on every remove button *(Carbon, Atlassian)*
|
|
154
|
+
- Show file name, size, and status in the file list *(Polaris, Carbon, Atlassian)*
|
|
155
|
+
- Announce upload errors via `aria-live="assertive"` *(WCAG 2.1 §4.1.3 Status Messages)*
|
|
156
|
+
|
|
157
|
+
### Don't
|
|
158
|
+
- Don't use `display:none` on the file input — keyboard and AT users cannot trigger the picker *(WCAG 2.1 §2.1.1)*
|
|
159
|
+
- Don't omit the `accept` hint text — users should know allowed types before selecting *(Polaris, Carbon)*
|
|
160
|
+
- Don't show only drag-drop UI with no browse button — drag is inaccessible to keyboard users *(Carbon, Atlassian)*
|
|
161
|
+
- Don't use a generic `aria-label="Remove"` on remove buttons — AT users cannot identify which file *(Carbon)*
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Anti-patterns Cross-links
|
|
166
|
+
|
|
167
|
+
| Anti-pattern | Entry |
|
|
168
|
+
|--------------|-------|
|
|
169
|
+
| BAN-08 | File input hidden with display:none — keyboard inaccessible — `reference/anti-patterns.md#ban-08` |
|
|
170
|
+
| BAN-13 | Icon-only action button without aria-label — `reference/anti-patterns.md#ban-13` |
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Benchmark Citations
|
|
175
|
+
|
|
176
|
+
| Claim | Sources |
|
|
177
|
+
|-------|---------|
|
|
178
|
+
| input type="file" must not be display:none | WCAG 2.1 §2.1.1, Carbon, Polaris accessibility guides |
|
|
179
|
+
| Remove button needs aria-label="Remove [filename]" | Carbon FileUploader, Atlassian, Polaris |
|
|
180
|
+
| Upload errors need aria-live="assertive" | WCAG 2.1 §4.1.3, Material 3 |
|
|
181
|
+
| Progress bar needs aria-valuenow updates | WAI-ARIA progressbar role spec |
|
|
182
|
+
| Drag-over state: background fill + border change | Polaris, Carbon, Atlassian drop zone specs |
|
|
183
|
+
|
|
184
|
+
Full system URLs: `connections/design-corpora.md`
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Grep Signatures
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
# File input hidden with display:none (keyboard inaccessible)
|
|
192
|
+
grep -rn 'type="file"' src/ | grep -v 'opacity\|position.*absolute' | grep 'display.*none\|visibility.*hidden'
|
|
193
|
+
|
|
194
|
+
# Remove button missing aria-label (icon-only, no accessible name)
|
|
195
|
+
grep -rn 'remove.*button\|btn.*remove\|✕\|×' src/ | grep -v 'aria-label'
|
|
196
|
+
|
|
197
|
+
# Progress bar missing aria-valuenow
|
|
198
|
+
grep -rn 'role="progressbar"' src/ | grep -v 'aria-valuenow'
|
|
199
|
+
|
|
200
|
+
# Upload error region without aria-live
|
|
201
|
+
grep -rn 'upload.*error\|file.*error\|error.*upload' src/ | grep -v 'aria-live'
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Failing Example
|
|
207
|
+
|
|
208
|
+
```html
|
|
209
|
+
<!-- BAD: drop zone with display:none on the actual <input> — keyboard and AT users can't trigger file picker -->
|
|
210
|
+
<div class="drop-zone" ondrop="handleDrop(event)" ondragover="handleDragOver(event)">
|
|
211
|
+
<p>Drag files here</p>
|
|
212
|
+
<input type="file" id="file-input" style="display:none" onchange="handleFiles(event)">
|
|
213
|
+
<button onclick="document.getElementById('file-input').click()">Browse</button>
|
|
214
|
+
</div>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Why it fails**: `display:none` removes the input from accessibility tree and tab order. The JavaScript `.click()` workaround does not work reliably with all AT. Keyboard users pressing Enter/Space on "Browse" may get inconsistent behavior across browsers. Screen reader users cannot discover or activate the file input directly.
|
|
218
|
+
**Grep detection**: `grep -rn 'type="file".*display.*none\|display.*none.*type="file"' src/`
|
|
219
|
+
**Fix**: Use `opacity:0; position:absolute; width:100%; height:100%` on the input (matching the browse button dimensions), or place a visible `<input type="file">` and style the button as a `<label for="file-input">` so clicking the label activates the input natively without JavaScript.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Input — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Material 3, Carbon, Ant Design, Mantine, Polaris, Fluent 2, Atlassian, shadcn/ui
|
|
4
|
+
**Wave**: 1 · **Category**: Inputs
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
A single-line text input collects short textual data from the user. It always has an associated visible label (never placeholder-only), optionally shows a helper text or character count below, and surfaces error state with an accessible message. Multi-line content belongs in a textarea; structured data (dates, phones) may warrant a specialised input type.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Anatomy
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
Label * ← <label for="id"> — always visible
|
|
18
|
+
┌─────────────────────────┐
|
|
19
|
+
│ placeholder / value │ ← <input type="text"> or type="search" / "email" / etc.
|
|
20
|
+
└─────────────────────────┘
|
|
21
|
+
Helper text / Error msg ← aria-describedby linked
|
|
22
|
+
Character count (opt.) ← aria-live="polite" region
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
| Part | Required | Notes |
|
|
26
|
+
|------|----------|-------|
|
|
27
|
+
| Label | Yes | Visible; never placeholder-only |
|
|
28
|
+
| Input element | Yes | Native `<input>` preferred; `type` set explicitly |
|
|
29
|
+
| Helper text | No | Persistent instructional text below input |
|
|
30
|
+
| Error message | Conditional | Shown on invalid state; replaces or joins helper text |
|
|
31
|
+
| Character count | No | `aria-live="polite"` region; announced on pause |
|
|
32
|
+
| Required indicator | No | `*` with `aria-required="true"` on input; legend explains `*` |
|
|
33
|
+
| Leading icon / adornment | No | 16–20px; left-inset with 12px gap from text |
|
|
34
|
+
| Trailing icon / clear button | No | Clear action must be keyboard-accessible |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Variants
|
|
39
|
+
|
|
40
|
+
| Variant | Description | Systems |
|
|
41
|
+
|---------|-------------|---------|
|
|
42
|
+
| Outlined | Border box with floating/static label | Material 3, Ant, Mantine, shadcn |
|
|
43
|
+
| Filled | Filled background, underline only | Material 3, Carbon |
|
|
44
|
+
| Underline / Simple | Bottom border only | Carbon (fluid), Fluent |
|
|
45
|
+
| Search | Leading search icon; clear button on value | All systems |
|
|
46
|
+
| Password | Trailing show/hide toggle | All systems |
|
|
47
|
+
| Number | `type="number"` or `inputmode="numeric"` | Material 3, Ant, Mantine |
|
|
48
|
+
|
|
49
|
+
**Norm** (≥6/18): outlined with floating or static label is the most-cited default.
|
|
50
|
+
**Diverge**: floating vs. static label — Material 3 uses floating; Carbon, Polaris, Atlassian use static (above). Static label is safer for a11y (floating requires JavaScript + ARIA management).
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## States
|
|
55
|
+
|
|
56
|
+
| State | Trigger | Visual | ARIA |
|
|
57
|
+
|-------|---------|--------|------|
|
|
58
|
+
| default | — | Resting border | — |
|
|
59
|
+
| hover | pointer over | Border lightens 20% | — |
|
|
60
|
+
| focus | keyboard / click | 2px focus-visible ring or thickened border | — |
|
|
61
|
+
| filled | has value | Label lifts (floating) or stays static | — |
|
|
62
|
+
| disabled | `disabled` attr | 38% opacity; cursor: not-allowed | `disabled` attr |
|
|
63
|
+
| read-only | `readonly` attr | No border change; cursor: default | `readonly` attr |
|
|
64
|
+
| error | invalid | Red/error border + icon + error message | `aria-invalid="true"` + `aria-describedby` |
|
|
65
|
+
| success | valid (opt.) | Green border + check icon | — |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Sizing & Spacing
|
|
70
|
+
|
|
71
|
+
| Size | Height | Padding H | Font | Label size |
|
|
72
|
+
|------|--------|-----------|------|------------|
|
|
73
|
+
| sm | 32px | 12px | 13px | 12px |
|
|
74
|
+
| md (default) | 40px | 16px | 14px | 14px |
|
|
75
|
+
| lg | 48px | 16px | 16px | 16px |
|
|
76
|
+
|
|
77
|
+
**Norm**: 40px default height (Carbon, Polaris, Fluent, Atlassian confirm).
|
|
78
|
+
Minimum width: 200px — narrower inputs invite input truncation and frustrate users.
|
|
79
|
+
|
|
80
|
+
Cross-link: `reference/surfaces.md` — hit area ≥44px via padding; `reference/typography.md` — label sizing.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Typography
|
|
85
|
+
|
|
86
|
+
- Label: 14px/500 above input; 12px when floating in focus/filled state
|
|
87
|
+
- Placeholder: 14px/400; color at 40% contrast minimum — never the only label
|
|
88
|
+
- Helper/error: 12px/400; full contrast for error messages
|
|
89
|
+
- **Placeholder is not a label**: it disappears on type, fails contrast, and cannot be announced by screen readers as a persistent label
|
|
90
|
+
|
|
91
|
+
Cross-link: `reference/typography.md` — text-wrap, font-smoothing rules
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Keyboard & Accessibility
|
|
96
|
+
|
|
97
|
+
> **WAI-ARIA role**: `textbox` (implicit on `<input type="text">`)
|
|
98
|
+
> **Required attributes**: `id` + matching `<label for>`, or `aria-label`; `aria-describedby` linking error/helper
|
|
99
|
+
|
|
100
|
+
### Keyboard Contract
|
|
101
|
+
|
|
102
|
+
*Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/textbox/ — W3C — 2024*
|
|
103
|
+
|
|
104
|
+
| Key | Action |
|
|
105
|
+
|-----|--------|
|
|
106
|
+
| Any printable character | Types character into field |
|
|
107
|
+
| Backspace / Delete | Removes character |
|
|
108
|
+
| Home | Moves caret to start |
|
|
109
|
+
| End | Moves caret to end |
|
|
110
|
+
| Ctrl+A | Selects all |
|
|
111
|
+
| Tab | Moves focus to next element |
|
|
112
|
+
| Shift+Tab | Moves focus to previous element |
|
|
113
|
+
|
|
114
|
+
Password toggle and clear button must be keyboard accessible (Enter/Space activate).
|
|
115
|
+
|
|
116
|
+
### Accessibility Rules
|
|
117
|
+
|
|
118
|
+
- Label MUST be associated via `<label for="id">` or `aria-label` — `placeholder` alone is not sufficient
|
|
119
|
+
- Error message MUST be linked via `aria-describedby` and triggered before or alongside visual indicator
|
|
120
|
+
- `aria-invalid="true"` MUST be set on the input when in error state
|
|
121
|
+
- `aria-required="true"` for required fields (supplement with visual `*` + legend)
|
|
122
|
+
- Character count region: `aria-live="polite"` to avoid over-announcing on every keystroke
|
|
123
|
+
|
|
124
|
+
Cross-link: `reference/accessibility.md`
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Motion
|
|
129
|
+
|
|
130
|
+
| Transition | Duration | Easing | Notes |
|
|
131
|
+
|------------|----------|--------|-------|
|
|
132
|
+
| label float | 150ms | ease-out | Floating label only; avoid if complex JS needed |
|
|
133
|
+
| border colour | 100ms | ease | Focus/error state border change |
|
|
134
|
+
| error message in | 150ms | ease-out | Slide-down + fade; respect prefers-reduced-motion |
|
|
135
|
+
|
|
136
|
+
Cross-link: `reference/motion.md` — `prefers-reduced-motion` guard required on label float
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Do / Don't
|
|
141
|
+
|
|
142
|
+
### Do
|
|
143
|
+
- Always show a visible label above or beside the input *(Carbon, Polaris, Atlassian, WAI-ARIA APG)*
|
|
144
|
+
- Show inline error messages immediately below the failing field *(Material 3, Carbon, Polaris)*
|
|
145
|
+
- Associate helper text and errors via `aria-describedby` *(WAI-ARIA APG)*
|
|
146
|
+
- Use `autocomplete` attributes for common fields (name, email, address) *(Polaris, Fluent)*
|
|
147
|
+
|
|
148
|
+
### Don't
|
|
149
|
+
- Don't use `placeholder` as the only label — it disappears and fails contrast *(Carbon, Polaris, Atlassian)*
|
|
150
|
+
- Don't show error state before the user has had a chance to input (premature validation) *(Polaris)*
|
|
151
|
+
- Don't remove the label on focus to create space — floating labels break screen readers *(Atlassian)*
|
|
152
|
+
- Don't use `type="number"` for things that aren't math operands (phone, ZIP) — use `inputmode` instead *(Mantine, Carbon)*
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Anti-patterns Cross-links
|
|
157
|
+
|
|
158
|
+
| Anti-pattern | Entry |
|
|
159
|
+
|--------------|-------|
|
|
160
|
+
| Placeholder-as-label | `reference/anti-patterns.md` — no dedicated BAN yet; cross-ref accessibility.md |
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Benchmark Citations
|
|
165
|
+
|
|
166
|
+
| Claim | Sources |
|
|
167
|
+
|-------|---------|
|
|
168
|
+
| 40px default height | Carbon, Polaris, Fluent 2, Atlassian |
|
|
169
|
+
| Placeholder not a label | Carbon, Polaris, Atlassian, WAI-ARIA APG |
|
|
170
|
+
| aria-describedby for errors | WAI-ARIA APG, Carbon, Mantine |
|
|
171
|
+
| Static label safer than floating | Atlassian, Carbon, Polaris |
|
|
172
|
+
|
|
173
|
+
Full system URLs: `connections/design-corpora.md`
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Grep Signatures
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# Placeholder-as-label (no <label> associated)
|
|
181
|
+
grep -rn 'placeholder=' src/ | grep -v 'aria-label\|<label'
|
|
182
|
+
|
|
183
|
+
# Missing aria-invalid on error state
|
|
184
|
+
grep -rn 'error\|invalid' src/ | grep '<input' | grep -v 'aria-invalid'
|
|
185
|
+
|
|
186
|
+
# Missing aria-describedby on input with helper/error
|
|
187
|
+
grep -rn '<input' src/ | grep -v 'aria-describedby'
|
|
188
|
+
|
|
189
|
+
# type="number" on non-numeric semantic fields
|
|
190
|
+
grep -rn 'type="number"' src/ | grep -i 'phone\|zip\|postal\|card'
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Failing Example
|
|
196
|
+
|
|
197
|
+
```html
|
|
198
|
+
<!-- BAD: placeholder as label — disappears on type, fails contrast, not announced persistently -->
|
|
199
|
+
<input type="text" placeholder="Email address" />
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Why it fails**: Placeholder has 40% opacity (below 4.5:1 AA), disappears when user types, and screen readers do not treat it as a persistent label.
|
|
203
|
+
**Grep detection**: `grep -rn '<input' src/ | grep 'placeholder=' | grep -v 'aria-label\|id='`
|
|
204
|
+
**Fix**:
|
|
205
|
+
```html
|
|
206
|
+
<label for="email">Email address</label>
|
|
207
|
+
<input type="email" id="email" autocomplete="email" />
|
|
208
|
+
```
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Label — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: WAI-ARIA APG, Carbon, Material 3, Mantine, Polaris, Atlassian, Fluent 2, shadcn/ui
|
|
4
|
+
**Wave**: 1 · **Category**: Inputs
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
A label is the visible text that identifies a form control to the user and to assistive technology. It is the most critical accessibility primitive in forms — every input, select, checkbox, radio, and switch MUST have an associated label. Labels are distinct from placeholders (which disappear) and from hints (which supplement but do not replace). *(WAI-ARIA APG, Carbon, Polaris, Atlassian all agree)*
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Anatomy
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
Label text * ← <label for="id"> (static, above control)
|
|
18
|
+
┌──────────────────┐
|
|
19
|
+
│ Input │ ← <input id="id">
|
|
20
|
+
└──────────────────┘
|
|
21
|
+
Helper text
|
|
22
|
+
|
|
23
|
+
Alternative (legend for group):
|
|
24
|
+
<fieldset>
|
|
25
|
+
<legend>Group label</legend> ← <legend> replaces <label> for groups
|
|
26
|
+
...controls...
|
|
27
|
+
</fieldset>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Part | Required | Notes |
|
|
31
|
+
|------|----------|-------|
|
|
32
|
+
| Label text | Yes | Visible; descriptive; ≤40 chars preferred |
|
|
33
|
+
| Required indicator | Conditional | `*` or "(required)"; always explained near form |
|
|
34
|
+
| Optional indicator | Conditional | "(optional)" text is clearer than required asterisk |
|
|
35
|
+
| Helper text | No | Below control; `aria-describedby` |
|
|
36
|
+
| `for` / `id` association | Yes | OR `aria-label` / `aria-labelledby` on control |
|
|
37
|
+
| Legend (groups) | Yes (groups) | Replaces `<label>` for `<fieldset>` groups |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Variants
|
|
42
|
+
|
|
43
|
+
| Variant | Description | Systems |
|
|
44
|
+
|---------|-------------|---------|
|
|
45
|
+
| Static label (above) | Fixed position above control — most accessible | Carbon, Polaris, Atlassian, Fluent |
|
|
46
|
+
| Floating label | Starts inside control, floats up on focus/fill | Material 3, Mantine, shadcn |
|
|
47
|
+
| Inline label | Label beside control (radio/checkbox) | All |
|
|
48
|
+
| Legend | Group label inside `<fieldset>` | WAI-ARIA APG, all (for groups) |
|
|
49
|
+
| Visually hidden | Accessible but not visible (e.g., search icon button) | WAI-ARIA APG, Carbon |
|
|
50
|
+
|
|
51
|
+
**Norm** (≥5/18): static label above the control is the most accessible and implementation-simple approach — recommended as the default.
|
|
52
|
+
**Diverge**: floating label — Material 3 and Mantine use it; Carbon, Polaris, Atlassian explicitly recommend static labels for a11y predictability. Floating labels require JavaScript, break if JS fails, and require careful `aria-*` management.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## States
|
|
57
|
+
|
|
58
|
+
Labels are not interactive — they have no hover/focus states of their own. However:
|
|
59
|
+
|
|
60
|
+
| Control State | Label Behaviour |
|
|
61
|
+
|---------------|-----------------|
|
|
62
|
+
| error | Label colour may shift to error colour (optional); error text replaces/appends helper |
|
|
63
|
+
| disabled | Label at 38% opacity alongside disabled control |
|
|
64
|
+
| required | Required indicator (`*`) added — never remove from DOM |
|
|
65
|
+
| focus (on control) | Label may shift colour to primary (Material 3 floating) |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Sizing & Spacing
|
|
70
|
+
|
|
71
|
+
| Property | Value | Notes |
|
|
72
|
+
|----------|-------|-------|
|
|
73
|
+
| Font size | 14px (static); 12px (floating — small state) | |
|
|
74
|
+
| Weight | 500 | Slightly heavier than body to distinguish |
|
|
75
|
+
| Gap: label → control | 4–8px | *(Carbon: 4px, Material 3: 8px)* |
|
|
76
|
+
| Required asterisk gap | 2px left of asterisk | |
|
|
77
|
+
| Width | Match control width | Labels should not exceed their control |
|
|
78
|
+
|
|
79
|
+
Cross-link: `reference/typography.md` — label sizing rules
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Typography
|
|
84
|
+
|
|
85
|
+
- Label text: 14px/500 — slightly heavier than body 400; distinguishes from surrounding content
|
|
86
|
+
- Required `*`: same size, colour matches error or primary brand colour
|
|
87
|
+
- Visually-hidden labels: use CSS `.sr-only` pattern (clip + overflow: hidden + absolute), never `display:none` or `visibility:hidden`
|
|
88
|
+
|
|
89
|
+
```css
|
|
90
|
+
/* sr-only — label hidden visually but present for screen readers */
|
|
91
|
+
.sr-only {
|
|
92
|
+
position: absolute;
|
|
93
|
+
width: 1px; height: 1px;
|
|
94
|
+
padding: 0; margin: -1px;
|
|
95
|
+
overflow: hidden;
|
|
96
|
+
clip: rect(0,0,0,0);
|
|
97
|
+
white-space: nowrap;
|
|
98
|
+
border: 0;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Keyboard & Accessibility
|
|
105
|
+
|
|
106
|
+
> **WAI-ARIA role**: `label` (implicit on `<label>`)
|
|
107
|
+
> **Required attributes**: `for="control-id"` on `<label>`, matching `id` on control
|
|
108
|
+
|
|
109
|
+
### Association Methods (in order of preference)
|
|
110
|
+
|
|
111
|
+
*Per WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/ — W3C — 2024*
|
|
112
|
+
|
|
113
|
+
1. **`<label for="id">`** — native HTML; best browser + AT support; clicking label focuses control
|
|
114
|
+
2. **`aria-labelledby="label-id"`** — when label cannot use `for` (complex composites)
|
|
115
|
+
3. **`aria-label="string"`** — when no visible label is possible (icon-only controls); last resort
|
|
116
|
+
4. **`<legend>` inside `<fieldset>`** — for groups of related controls; not replaceable by `aria-label`
|
|
117
|
+
|
|
118
|
+
### Accessibility Rules
|
|
119
|
+
|
|
120
|
+
- NEVER use `placeholder` as the only label — it disappears on input and fails colour contrast *(WAI-ARIA APG, WCAG 1.3.1)*
|
|
121
|
+
- Required fields: mark with `aria-required="true"` on the control AND `*` visually; provide a form-level note explaining the `*` convention
|
|
122
|
+
- Optional fields: prefer marking optional fields with "(optional)" text over marking every required field with `*` — reduces asterisk clutter in long forms *(Polaris, Carbon)*
|
|
123
|
+
- Group labels: `<legend>` inside `<fieldset>` is the ONLY proper group label technique — `aria-label` on a `<div>` group is inadequate for radio/checkbox groups in most AT
|
|
124
|
+
- Visually hidden labels: use `.sr-only` CSS — never `display:none` (removes from AT tree) or `visibility:hidden`
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Do / Don't
|
|
129
|
+
|
|
130
|
+
### Do
|
|
131
|
+
- Place labels above controls, not beside them, for forms wider than 240px *(Carbon, Polaris, Atlassian)*
|
|
132
|
+
- Use `<label for="id">` — the click zone extends to the full label, improving usability *(WAI-ARIA APG, all)*
|
|
133
|
+
- Explain the `*` required indicator once near the top of the form *(Polaris, Carbon)*
|
|
134
|
+
- Use `<legend>` for groups — it is read before each option in the group *(WAI-ARIA APG)*
|
|
135
|
+
|
|
136
|
+
### Don't
|
|
137
|
+
- Don't use `placeholder` as the only label — it fails at 3 accessibility criteria *(WAI-ARIA APG, WCAG 1.3.1, 1.4.3)*
|
|
138
|
+
- Don't use `display:none` on labels — removes them from the AT accessibility tree *(WAI-ARIA APG)*
|
|
139
|
+
- Don't write labels as questions ("What is your name?") — prefer noun phrases ("Full name") *(Polaris, Carbon)*
|
|
140
|
+
- Don't truncate label text — ellipsis hides required information from all users *(Atlassian, Carbon)*
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Anti-patterns Cross-links
|
|
145
|
+
|
|
146
|
+
| Anti-pattern | Entry |
|
|
147
|
+
|--------------|-------|
|
|
148
|
+
| Placeholder-as-label | `reference/anti-patterns.md` |
|
|
149
|
+
| display:none on accessible label | `reference/anti-patterns.md` |
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Benchmark Citations
|
|
154
|
+
|
|
155
|
+
| Claim | Sources |
|
|
156
|
+
|-------|---------|
|
|
157
|
+
| `<label for>` clicking focuses control | HTML spec, WAI-ARIA APG |
|
|
158
|
+
| legend for group labels (not aria-label) | WAI-ARIA APG, Carbon |
|
|
159
|
+
| Static label above preferred over floating | Carbon, Polaris, Atlassian |
|
|
160
|
+
| .sr-only pattern for hidden labels | WAI-ARIA APG, Carbon, Tailwind |
|
|
161
|
+
| placeholder fails 3 a11y criteria | WAI-ARIA APG, WCAG 1.3.1, 1.4.3 |
|
|
162
|
+
|
|
163
|
+
Full system URLs: `connections/design-corpora.md`
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Grep Signatures
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# Input with no associated label (no for/id, no aria-label)
|
|
171
|
+
grep -rn '<input' src/ | grep -v 'type="hidden"\|type="submit"\|type="button"' \
|
|
172
|
+
| grep -v 'id=\|aria-label\|aria-labelledby'
|
|
173
|
+
|
|
174
|
+
# Label using display:none (removed from AT tree)
|
|
175
|
+
grep -rn 'display:\s*none\|display:none' src/ | grep -i 'label\|<label'
|
|
176
|
+
|
|
177
|
+
# Placeholder used without separate label
|
|
178
|
+
grep -rn 'placeholder=' src/ | grep -v 'aria-label\|<label\|aria-labelledby'
|
|
179
|
+
|
|
180
|
+
# Group without fieldset/legend
|
|
181
|
+
grep -rn 'type="radio"\|type="checkbox"' src/ | grep -v 'fieldset\|legend'
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Failing Example
|
|
187
|
+
|
|
188
|
+
```html
|
|
189
|
+
<!-- BAD: label using display:none — completely removed from accessibility tree -->
|
|
190
|
+
<label for="search" style="display:none">Search</label>
|
|
191
|
+
<input type="text" id="search" placeholder="Search…">
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Why it fails**: `display:none` removes the label from the DOM accessibility tree. Screen readers see only the placeholder (which disappears on type and has low contrast). The input has no persistent accessible name.
|
|
195
|
+
**Grep detection**: `grep -rn 'display:.*none' src/ | grep '<label\|label.*for'`
|
|
196
|
+
**Fix**:
|
|
197
|
+
```html
|
|
198
|
+
<label for="search" class="sr-only">Search</label>
|
|
199
|
+
<input type="text" id="search" placeholder="Search products…">
|
|
200
|
+
```
|