@hegemonart/get-design-done 1.14.8 → 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.
Files changed (58) hide show
  1. package/.claude-plugin/marketplace.json +5 -3
  2. package/.claude-plugin/plugin.json +15 -5
  3. package/CHANGELOG.md +97 -0
  4. package/README.md +30 -0
  5. package/SKILL.md +4 -1
  6. package/agents/a11y-mapper.md +25 -0
  7. package/agents/component-benchmark-harvester.md +112 -0
  8. package/agents/component-benchmark-synthesizer.md +88 -0
  9. package/agents/design-auditor.md +92 -8
  10. package/agents/design-context-builder.md +6 -0
  11. package/agents/design-executor.md +5 -2
  12. package/agents/design-pattern-mapper.md +2 -0
  13. package/agents/design-verifier.md +11 -0
  14. package/agents/motion-mapper.md +45 -0
  15. package/agents/token-mapper.md +36 -0
  16. package/agents/visual-hierarchy-mapper.md +29 -0
  17. package/connections/design-corpora.md +158 -0
  18. package/package.json +16 -2
  19. package/reference/anti-patterns.md +69 -0
  20. package/reference/audit-scoring.md +34 -3
  21. package/reference/brand-voice.md +199 -0
  22. package/reference/checklists.md +30 -3
  23. package/reference/components/README.md +90 -0
  24. package/reference/components/TEMPLATE.md +184 -0
  25. package/reference/components/accordion.md +217 -0
  26. package/reference/components/button.md +195 -0
  27. package/reference/components/card.md +200 -0
  28. package/reference/components/checkbox.md +207 -0
  29. package/reference/components/drawer.md +201 -0
  30. package/reference/components/input.md +208 -0
  31. package/reference/components/label.md +200 -0
  32. package/reference/components/link.md +193 -0
  33. package/reference/components/modal-dialog.md +210 -0
  34. package/reference/components/popover.md +197 -0
  35. package/reference/components/radio.md +203 -0
  36. package/reference/components/select-combobox.md +219 -0
  37. package/reference/components/switch.md +194 -0
  38. package/reference/components/tabs.md +213 -0
  39. package/reference/components/tooltip.md +201 -0
  40. package/reference/data/google-fonts.csv +51 -0
  41. package/reference/data/palettes.csv +41 -0
  42. package/reference/data/styles.csv +39 -0
  43. package/reference/design-system-guidance.md +177 -0
  44. package/reference/design-systems-catalog.md +151 -0
  45. package/reference/framer-motion-patterns.md +411 -0
  46. package/reference/gestalt.md +219 -0
  47. package/reference/iconography.md +231 -0
  48. package/reference/motion.md +102 -0
  49. package/reference/palette-catalog.md +82 -0
  50. package/reference/performance.md +304 -0
  51. package/reference/registry.json +359 -28
  52. package/reference/registry.schema.json +2 -1
  53. package/reference/review-format.md +2 -2
  54. package/reference/style-vocabulary.md +62 -0
  55. package/reference/surfaces.md +114 -0
  56. package/reference/typography.md +80 -0
  57. package/reference/visual-hierarchy-layout.md +306 -0
  58. 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
+ ```