@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,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
|
+
```
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Link — Benchmark Spec
|
|
2
|
+
|
|
3
|
+
**Harvested from**: Carbon, Polaris, Primer (GitHub), Fluent 2, WAI-ARIA APG, Material 3, Mantine, Atlassian
|
|
4
|
+
**Wave**: 1 · **Category**: Inputs
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
A link navigates the user to a new resource — another page, section, or external URL. It is the semantic counterpart to Button: if clicking takes the user somewhere, it is a `<a href>` link; if it triggers an action in the current context, it is a button. Never reverse these roles. *(Carbon, Primer, WAI-ARIA APG all enforce this boundary)*
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Anatomy
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
Inline: Read the [full documentation] for details.
|
|
18
|
+
└── <a href="..."> — inline, within body text
|
|
19
|
+
|
|
20
|
+
Standalone: [→ View report]
|
|
21
|
+
└── <a href="..."> — block-level, not inline in prose
|
|
22
|
+
|
|
23
|
+
External: [Open dashboard ↗]
|
|
24
|
+
└── <a href="..." target="_blank" rel="noopener noreferrer">
|
|
25
|
+
+ aria-label="Open dashboard (opens in new tab)"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Part | Required | Notes |
|
|
29
|
+
|------|----------|-------|
|
|
30
|
+
| `<a href>` | Yes | Native element; href MUST be a real URL or `#anchor` |
|
|
31
|
+
| Visible label | Yes | Descriptive of destination — not "click here" or "read more" |
|
|
32
|
+
| Underline | Conditional | Required for inline links in body text; optional for standalone/nav |
|
|
33
|
+
| Visited state | No | Encouraged for inline links in long-form content |
|
|
34
|
+
| External icon | Conditional | Required when `target="_blank"`; 12–14px, inline-aligned |
|
|
35
|
+
| `rel="noopener noreferrer"` | Yes (external) | Security — prevents opener access |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Variants
|
|
40
|
+
|
|
41
|
+
| Variant | Description | Systems |
|
|
42
|
+
|---------|-------------|---------|
|
|
43
|
+
| Inline | Within prose; always underlined | All |
|
|
44
|
+
| Standalone | Block-level CTA; underline optional | Carbon, Polaris, Primer |
|
|
45
|
+
| Nav / breadcrumb | In navigation contexts; no underline | All |
|
|
46
|
+
| External | `target="_blank"` with external icon | All |
|
|
47
|
+
| Destructive | Rare; red colour for delete-via-link patterns | Polaris (critical link) |
|
|
48
|
+
| Disabled | `aria-disabled="true"` + `tabindex="-1"` | Carbon, Primer |
|
|
49
|
+
|
|
50
|
+
**Norm** (≥6/18): inline links in body text MUST be underlined — colour alone fails WCAG 1.4.1.
|
|
51
|
+
**Diverge**: visited state — Primer and Carbon use it for documentation; most SaaS systems omit it.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## States
|
|
56
|
+
|
|
57
|
+
| State | Visual | ARIA / HTML |
|
|
58
|
+
|-------|--------|-------------|
|
|
59
|
+
| default | Underline + colour | `href` present |
|
|
60
|
+
| hover | Colour shift (10% darker/lighter) | — |
|
|
61
|
+
| focus | 2px focus-visible ring | — |
|
|
62
|
+
| active / pressed | Colour darkens + subtle scale 0.98 | — |
|
|
63
|
+
| visited | Distinct colour (purple conventional) | `:visited` pseudo-class |
|
|
64
|
+
| disabled | 38% opacity; pointer-events: none | `aria-disabled="true"` + `tabindex="-1"` |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Sizing & Spacing
|
|
69
|
+
|
|
70
|
+
- Links inherit parent font-size and line-height — do not override
|
|
71
|
+
- Standalone link min-height: 44px via padding for touch targets
|
|
72
|
+
- Icon size (external/leading): 12–14px; `vertical-align: middle`; 4px gap from text
|
|
73
|
+
|
|
74
|
+
Cross-link: `reference/surfaces.md` — hit-area pattern for standalone links
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Typography
|
|
79
|
+
|
|
80
|
+
- Inline links: same weight as surrounding text (400); underline distinguishes them
|
|
81
|
+
- Standalone links: 400–500 weight; may have leading icon or trailing arrow
|
|
82
|
+
- Never use ALL CAPS for links — reduces readability and implies different semantics
|
|
83
|
+
- Truncate with ellipsis + `title` attribute only when space is genuinely constrained
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Keyboard & Accessibility
|
|
88
|
+
|
|
89
|
+
> **WAI-ARIA role**: `link` (implicit on `<a href>`)
|
|
90
|
+
> **Required attributes**: `href` — without it, the element is not a link and has no keyboard access
|
|
91
|
+
|
|
92
|
+
### Keyboard Contract
|
|
93
|
+
|
|
94
|
+
*Quoted verbatim from WAI-ARIA APG — https://www.w3.org/WAI/ARIA/apg/patterns/link/ — W3C — 2024*
|
|
95
|
+
|
|
96
|
+
| Key | Action |
|
|
97
|
+
|-----|--------|
|
|
98
|
+
| Enter | Activates the link and navigates to its destination |
|
|
99
|
+
| Tab | Moves focus to the next focusable element |
|
|
100
|
+
| Shift+Tab | Moves focus to the previous focusable element |
|
|
101
|
+
|
|
102
|
+
### Accessibility Rules
|
|
103
|
+
|
|
104
|
+
- Link text MUST describe the destination — "click here" and "read more" fail 2.4.6 (descriptive labels)
|
|
105
|
+
- External links opening in new tab MUST disclose this: append "(opens in new tab)" to `aria-label`, or use a visually-hidden span
|
|
106
|
+
- `target="_blank"` MUST always be paired with `rel="noopener noreferrer"` (security + performance)
|
|
107
|
+
- Disabled links: `aria-disabled="true"` + `tabindex="-1"` — never `href=""` or `href="#"`
|
|
108
|
+
- Inline links in body text MUST be underlined — colour alone is insufficient for WCAG 1.4.1 (non-text contrast)
|
|
109
|
+
- Icon-only links (e.g., social icons) MUST have `aria-label` describing the destination
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Motion
|
|
114
|
+
|
|
115
|
+
| Transition | Duration | Easing | Notes |
|
|
116
|
+
|------------|----------|--------|-------|
|
|
117
|
+
| Colour on hover | 100ms | ease | Subtle; avoid opacity changes (readability) |
|
|
118
|
+
| Underline decoration | 80ms | ease | Underline grow or fade for standalone variants |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Do / Don't
|
|
123
|
+
|
|
124
|
+
### Do
|
|
125
|
+
- Use descriptive link text: "View account settings" not "click here" *(Polaris, Carbon, WAI-ARIA APG)*
|
|
126
|
+
- Underline inline links in body text *(Carbon, Polaris, WAI-ARIA APG — WCAG 1.4.1)*
|
|
127
|
+
- Add `rel="noopener noreferrer"` to all `target="_blank"` links *(Primer, Carbon, Fluent 2)*
|
|
128
|
+
- Disclose new-tab behavior in `aria-label` or visually-hidden text *(WAI-ARIA APG, Primer)*
|
|
129
|
+
|
|
130
|
+
### Don't
|
|
131
|
+
- Don't use `<a>` without `href` — it's not a link, not keyboard-accessible, and will confuse screen readers *(WAI-ARIA APG)*
|
|
132
|
+
- Don't use `<button>` when the action is navigation *(Carbon, Primer)*
|
|
133
|
+
- Don't rely on colour alone to distinguish links from surrounding text *(WCAG 1.4.1)*
|
|
134
|
+
- Don't open links in new tabs unexpectedly without disclosure *(Polaris, Primer, WCAG 3.2.5)*
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Anti-patterns Cross-links
|
|
139
|
+
|
|
140
|
+
| Anti-pattern | Entry |
|
|
141
|
+
|--------------|-------|
|
|
142
|
+
| Anchor without href | `reference/anti-patterns.md` |
|
|
143
|
+
| Non-descriptive link text | `reference/anti-patterns.md` |
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Benchmark Citations
|
|
148
|
+
|
|
149
|
+
| Claim | Sources |
|
|
150
|
+
|-------|---------|
|
|
151
|
+
| Enter activates link | WAI-ARIA APG §3.3 |
|
|
152
|
+
| Underline required for inline links | Carbon, Polaris, WCAG 1.4.1 |
|
|
153
|
+
| rel="noopener noreferrer" for _blank | Primer, Carbon, Fluent 2 |
|
|
154
|
+
| "click here" is anti-pattern | Polaris, Carbon, WAI-ARIA APG |
|
|
155
|
+
|
|
156
|
+
Full system URLs: `connections/design-corpora.md`
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Grep Signatures
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# <a> without href (not a link — no keyboard access)
|
|
164
|
+
grep -rn '<a ' src/ | grep -v 'href='
|
|
165
|
+
|
|
166
|
+
# target="_blank" without rel="noopener noreferrer"
|
|
167
|
+
grep -rn 'target="_blank"' src/ | grep -v 'rel=.*noopener'
|
|
168
|
+
|
|
169
|
+
# Non-descriptive link text
|
|
170
|
+
grep -rn '>click here\|>read more\|>learn more\|>here<' src/ | grep -i '<a'
|
|
171
|
+
|
|
172
|
+
# Colour-only link distinction (no text-decoration)
|
|
173
|
+
grep -rn 'text-decoration:\s*none' src/ | grep -i 'link\|<a'
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Failing Example
|
|
179
|
+
|
|
180
|
+
```html
|
|
181
|
+
<!-- BAD: non-descriptive link text + missing rel on external link -->
|
|
182
|
+
<a href="https://docs.example.com" target="_blank">Click here</a>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Why it fails**: "Click here" gives no destination context (fails WCAG 2.4.6). Missing `rel="noopener noreferrer"` is a security vulnerability. No disclosure of new-tab behavior.
|
|
186
|
+
**Grep detection**: `grep -rn '>click here\|>here<\|>read more' src/`
|
|
187
|
+
**Fix**:
|
|
188
|
+
```html
|
|
189
|
+
<a href="https://docs.example.com" target="_blank" rel="noopener noreferrer"
|
|
190
|
+
aria-label="View documentation (opens in new tab)">
|
|
191
|
+
View documentation <span aria-hidden="true">↗</span>
|
|
192
|
+
</a>
|
|
193
|
+
```
|