@bompus/house-rules 0.0.0-stage → 0.11.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/CONTRIBUTING.md +54 -0
- package/LICENSE +21 -0
- package/README.md +347 -2
- package/THIRD_PARTY_NOTICES.md +188 -0
- package/compose.mjs +119 -0
- package/composition.mjs +353 -0
- package/config-view.mjs +194 -0
- package/config.mjs +320 -0
- package/docs/assets/banner-dark.png +0 -0
- package/docs/assets/banner-light.png +0 -0
- package/docs/assets/overview.png +0 -0
- package/docs/configuration.md +193 -0
- package/docs/npm-release.md +45 -0
- package/docs/skill-names.md +50 -0
- package/examples/person/house-rules.json +15 -0
- package/examples/person/rules/10-tooling.md +8 -0
- package/examples/person/rules/20-reporting.md +9 -0
- package/examples/project/AGENTS.md +18 -0
- package/package.json +40 -4
- package/rule-references.mjs +91 -0
- package/rules/core.md +431 -0
- package/rules/modifiers/coded-offers.md +40 -0
- package/rules/modifiers/effort-estimates.md +41 -0
- package/rules/modifiers/land-when-done.md +12 -0
- package/rules/modifiers/low-quota-handoff.md +19 -0
- package/rules/modifiers/multi-agent.md +34 -0
- package/rules/modifiers/no-attribution.md +10 -0
- package/rules/modifiers/plan-files.md +50 -0
- package/rules/modifiers/question-cards.md +48 -0
- package/rules/modifiers/release-batching.md +13 -0
- package/rules/modifiers/scratch-on-disk.md +15 -0
- package/rules/modifiers/shared-host-load.md +48 -0
- package/rules/modifiers/solo-operator.md +13 -0
- package/rules/modifiers/squash-landing.md +22 -0
- package/rules/modifiers/swarmail.md +12 -0
- package/rules/references/github-actions.md +45 -0
- package/rules/references/landing.md +39 -0
- package/rules/references/multi-agent.md +47 -0
- package/rules/references/release-batching.md +43 -0
- package/rules/references/squash-landing.md +44 -0
- package/setup.mjs +375 -0
- package/skills/hr-agent-guidance-audit/LICENSE +21 -0
- package/skills/hr-agent-guidance-audit/SKILL.md +86 -0
- package/skills/hr-agent-guidance-audit/agents/openai.yaml +2 -0
- package/skills/hr-agent-guidance-audit/references/records-and-lessons.md +60 -0
- package/skills/hr-agent-guidance-refresh/LICENSE +21 -0
- package/skills/hr-agent-guidance-refresh/SKILL.md +68 -0
- package/skills/hr-agent-guidance-refresh/agents/openai.yaml +2 -0
- package/skills/hr-agent-guidance-refresh/references/verification.md +47 -0
- package/skills/hr-api-exposure-check/SKILL.md +159 -0
- package/skills/hr-api-exposure-check/agents/openai.yaml +3 -0
- package/skills/hr-audit-choices/SKILL.md +67 -0
- package/skills/hr-audit-choices/agents/openai.yaml +2 -0
- package/skills/hr-benchmarking/NOTICE.md +36 -0
- package/skills/hr-benchmarking/SKILL.md +316 -0
- package/skills/hr-benchmarking/agents/openai.yaml +2 -0
- package/skills/hr-benchmarking/references/javascript.md +17 -0
- package/skills/hr-benchmarking/references/priority-findings-20261006.md +115 -0
- package/skills/hr-benchmarking/references/tools.md +160 -0
- package/skills/hr-benchmarking/scripts/process-attribution.ps1 +20 -0
- package/skills/hr-benchmarking/scripts/process-attribution.py +107 -0
- package/skills/hr-better-accessibility/LICENSE +22 -0
- package/skills/hr-better-accessibility/NOTICE.md +46 -0
- package/skills/hr-better-accessibility/SKILL.md +119 -0
- package/skills/hr-better-accessibility/agents/openai.yaml +3 -0
- package/skills/hr-better-accessibility/focus-and-keyboard.md +130 -0
- package/skills/hr-better-accessibility/forms.md +82 -0
- package/skills/hr-better-accessibility/hit-areas.md +98 -0
- package/skills/hr-better-accessibility/motion-and-zoom.md +81 -0
- package/skills/hr-better-accessibility/screen-readers.md +101 -0
- package/skills/hr-better-accessibility/semantics-and-aria.md +84 -0
- package/skills/hr-better-colors/LICENSE +21 -0
- package/skills/hr-better-colors/NOTICE.md +37 -0
- package/skills/hr-better-colors/SKILL.md +114 -0
- package/skills/hr-better-colors/agents/openai.yaml +3 -0
- package/skills/hr-better-colors/color-formats.md +90 -0
- package/skills/hr-better-colors/color-usage.md +140 -0
- package/skills/hr-better-colors/contrast.md +80 -0
- package/skills/hr-better-colors/palette-generation.md +104 -0
- package/skills/hr-better-colors/palette-structure.md +76 -0
- package/skills/hr-better-colors/token-naming.md +97 -0
- package/skills/hr-better-layout/LICENSE +22 -0
- package/skills/hr-better-layout/NOTICE.md +48 -0
- package/skills/hr-better-layout/SKILL.md +92 -0
- package/skills/hr-better-layout/agents/openai.yaml +3 -0
- package/skills/hr-better-layout/grouping-and-alignment.md +144 -0
- package/skills/hr-better-layout/spacing-and-adaptivity.md +215 -0
- package/skills/hr-better-typography/LICENSE +22 -0
- package/skills/hr-better-typography/NOTICE.md +46 -0
- package/skills/hr-better-typography/SKILL.md +172 -0
- package/skills/hr-better-typography/agents/openai.yaml +3 -0
- package/skills/hr-better-typography/choosing-fonts.md +69 -0
- package/skills/hr-better-typography/css-cheat-sheet.md +65 -0
- package/skills/hr-better-typography/details-and-accessibility.md +119 -0
- package/skills/hr-better-typography/spacing-and-sizing.md +131 -0
- package/skills/hr-better-typography/variable-fonts-and-opentype.md +105 -0
- package/skills/hr-better-typography/wrapping-and-punctuation.md +57 -0
- package/skills/hr-better-writing/LICENSE +22 -0
- package/skills/hr-better-writing/NOTICE.md +46 -0
- package/skills/hr-better-writing/SKILL.md +122 -0
- package/skills/hr-better-writing/agents/openai.yaml +3 -0
- package/skills/hr-change-impact/SKILL.md +113 -0
- package/skills/hr-change-impact/agents/openai.yaml +5 -0
- package/skills/hr-code-review/LICENSE +21 -0
- package/skills/hr-code-review/NOTICE.md +26 -0
- package/skills/hr-code-review/SKILL.md +147 -0
- package/skills/hr-code-review/agents/openai.yaml +3 -0
- package/skills/hr-code-review/references/evidence.md +57 -0
- package/skills/hr-design-exploration/SKILL.md +82 -0
- package/skills/hr-design-exploration/agents/openai.yaml +4 -0
- package/skills/hr-diagnosing-bugs/NOTICE.md +45 -0
- package/skills/hr-diagnosing-bugs/SKILL.md +108 -0
- package/skills/hr-diagnosing-bugs/agents/openai.yaml +3 -0
- package/skills/hr-diagnosing-bugs/references/reporting.md +72 -0
- package/skills/hr-explain-code/NOTICE.md +37 -0
- package/skills/hr-explain-code/SKILL.md +110 -0
- package/skills/hr-explain-code/agents/openai.yaml +5 -0
- package/skills/hr-explain-code/references/helper-brief.md +39 -0
- package/skills/hr-extract-shared-steps/SKILL.md +134 -0
- package/skills/hr-extract-shared-steps/agents/openai.yaml +3 -0
- package/skills/hr-handoff/SKILL.md +91 -0
- package/skills/hr-handoff/agents/openai.yaml +5 -0
- package/skills/hr-handoff/assets/ledger.example.json +10 -0
- package/skills/hr-handoff/assets/receipt.example.json +76 -0
- package/skills/hr-handoff/assets/source.example.json +20 -0
- package/skills/hr-handoff/references/reconciliation.md +105 -0
- package/skills/hr-handoff/scripts/check-reconciliation.mjs +282 -0
- package/skills/hr-house-rules-setup/SKILL.md +100 -0
- package/skills/hr-house-rules-setup/agents/openai.yaml +5 -0
- package/skills/hr-issue-tracker-setup/SKILL.md +49 -0
- package/skills/hr-issue-tracker-setup/agents/openai.yaml +5 -0
- package/skills/hr-lean-plan/SKILL.md +142 -0
- package/skills/hr-lean-plan/agents/openai.yaml +5 -0
- package/skills/hr-maintainability-review/SKILL.md +176 -0
- package/skills/hr-maintainability-review/agents/openai.yaml +5 -0
- package/skills/hr-manual-qa/NOTICE.md +36 -0
- package/skills/hr-manual-qa/SKILL.md +37 -0
- package/skills/hr-manual-qa/agents/openai.yaml +3 -0
- package/skills/hr-navigation-retro/LICENSE +21 -0
- package/skills/hr-navigation-retro/NOTICE.md +5 -0
- package/skills/hr-navigation-retro/SKILL.md +86 -0
- package/skills/hr-navigation-retro/agents/openai.yaml +4 -0
- package/skills/hr-navigation-retro/references/inputs.md +67 -0
- package/skills/hr-navigation-retro/scripts/retro.py +552 -0
- package/skills/hr-navigation-retro/scripts/transcripts.py +205 -0
- package/skills/hr-ordering-tests/SKILL.md +51 -0
- package/skills/hr-ordering-tests/agents/openai.yaml +2 -0
- package/skills/hr-plain-prose/SKILL.md +261 -0
- package/skills/hr-plain-prose/agents/openai.yaml +3 -0
- package/skills/hr-plain-prose/scripts/check.mjs +331 -0
- package/skills/hr-pr-followup/LICENSE +21 -0
- package/skills/hr-pr-followup/NOTICE.md +9 -0
- package/skills/hr-pr-followup/SKILL.md +71 -0
- package/skills/hr-pr-followup/agents/openai.yaml +2 -0
- package/skills/hr-pr-followup/references/review-helpers.md +90 -0
- package/skills/hr-pr-followup/scripts/check-pr-review.ts +510 -0
- package/skills/hr-pr-followup/scripts/resolve-pr-thread.ts +219 -0
- package/skills/hr-progress-report/SKILL.md +49 -0
- package/skills/hr-progress-report/agents/openai.yaml +5 -0
- package/skills/hr-read-reddit/SKILL.md +44 -0
- package/skills/hr-read-reddit/agents/openai.yaml +2 -0
- package/skills/hr-read-reddit/scripts/reddit.ts +252 -0
- package/skills/hr-read-x-links/SKILL.md +42 -0
- package/skills/hr-read-x-links/agents/openai.yaml +2 -0
- package/skills/hr-read-x-links/scripts/x-post.ts +264 -0
- package/skills/hr-split-to-prs/LICENSE +21 -0
- package/skills/hr-split-to-prs/NOTICE.md +9 -0
- package/skills/hr-split-to-prs/SKILL.md +61 -0
- package/skills/hr-split-to-prs/agents/openai.yaml +2 -0
- package/skills/hr-stock-ui-audit/SKILL.md +132 -0
- package/skills/hr-stock-ui-audit/agents/openai.yaml +3 -0
- package/skills/hr-stock-ui-audit/scripts/scan.mjs +619 -0
- package/skills/hr-tdd/LICENSE +21 -0
- package/skills/hr-tdd/NOTICE.md +10 -0
- package/skills/hr-tdd/SKILL.md +40 -0
- package/skills/hr-tdd/agents/openai.yaml +3 -0
- package/skills/hr-tdd/mocking.md +39 -0
- package/skills/hr-tdd/tests.md +77 -0
- package/skills/hr-test-audit/SKILL.md +146 -0
- package/skills/hr-test-audit/agents/openai.yaml +3 -0
- package/skills/hr-test-audit/scripts/floor-guard.ts +137 -0
- package/skills/hr-what-next/SKILL.md +61 -0
- package/skills/hr-what-next/agents/openai.yaml +4 -0
- package/skills/hr-what-next/references/reporting.md +68 -0
- package/skills/hr-writing-for-agents/NOTICE.md +46 -0
- package/skills/hr-writing-for-agents/SKILL-MECHANICS.md +83 -0
- package/skills/hr-writing-for-agents/SKILL.md +108 -0
- package/skills/hr-writing-for-agents/agents/openai.yaml +3 -0
- package/skills/hr-writing-pr/NOTICE.md +48 -0
- package/skills/hr-writing-pr/SKILL.md +104 -0
- package/skills/hr-writing-pr/agents/openai.yaml +2 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Focus and keyboard
|
|
2
|
+
|
|
3
|
+
Focus rings, skip links, tabindex, focus trapping and the APG keyboard patterns.
|
|
4
|
+
|
|
5
|
+
## Focus rings
|
|
6
|
+
|
|
7
|
+
Style `:focus-visible`, not bare `:focus`. The browser shows it for keyboard and assistive-tech focus and suppresses it for mouse clicks, where focus is already obvious. Never write `outline: none` or `focus:outline-none` without a visible replacement, which removes keyboard navigation for sighted keyboard users.
|
|
8
|
+
|
|
9
|
+
Prefer the browser's unmodified focus indicator, which adapts to platform and forced-color settings without the author predicting every background. Adding only `outline-offset` preserves it. A custom `outline: 2px solid` with no color renders `currentColor`, which is not automatically accessible, because the outline may cross colors unlike the text's own background. The preference order:
|
|
10
|
+
|
|
11
|
+
```css
|
|
12
|
+
/* Best: keep the browser ring, just give it breathing room */
|
|
13
|
+
:focus-visible {
|
|
14
|
+
outline-offset: 2px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Custom ring when the design requires one: use the project's verified token */
|
|
18
|
+
:focus-visible {
|
|
19
|
+
outline: 2px solid var(--focus-ring);
|
|
20
|
+
outline-offset: 2px;
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
// Tailwind: use the project's focus token or established focus-ring utility
|
|
26
|
+
<button className="focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:[outline-style:solid] focus-visible:outline-[var(--focus-ring)]">
|
|
27
|
+
Save
|
|
28
|
+
</button>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
A custom focus indicator must meet the applicable project or WCAG target for visible area and change of contrast. Inspect the whole perimeter against every adjacent color it crosses: component fills, page surfaces, images, gradients, hover and selected states. A token, brand color, or `currentColor` passes only when that rendered check does.
|
|
32
|
+
|
|
33
|
+
In `forced-colors: active` (Windows High Contrast), keep the default color adjustment or name a system color such as `Highlight`. `forced-color-adjust: none` freezes the authored color, so use it only where you have checked the control stays perceivable.
|
|
34
|
+
|
|
35
|
+
Group focus styles with `:focus-within` when a wrapper should light up while an inner input has focus (e.g. a search box with an icon inside the border).
|
|
36
|
+
|
|
37
|
+
## Skip link
|
|
38
|
+
|
|
39
|
+
Target `<main id="main">` and visually hide the link until focused:
|
|
40
|
+
|
|
41
|
+
```css
|
|
42
|
+
.skip-link {
|
|
43
|
+
position: absolute;
|
|
44
|
+
inset-inline-start: -999px;
|
|
45
|
+
}
|
|
46
|
+
.skip-link:focus {
|
|
47
|
+
inset-inline-start: 16px;
|
|
48
|
+
top: 16px;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<body>
|
|
54
|
+
<a class="skip-link" href="#main">Skip to content</a>
|
|
55
|
+
<header>…</header>
|
|
56
|
+
<main id="main">…</main>
|
|
57
|
+
</body>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Give in-page anchor targets `scroll-margin-top`, such as `80px` under a sticky header, so the target isn't hidden when jumped to.
|
|
61
|
+
|
|
62
|
+
## tabindex rules
|
|
63
|
+
|
|
64
|
+
- `tabindex="0"`: adds an element to the natural tab order. Only for custom interactive elements that aren't natively focusable.
|
|
65
|
+
- `tabindex="-1"`: focusable via JavaScript only (`el.focus()`). Use for headings you move focus to, modal containers and roving-tabindex members.
|
|
66
|
+
- Positive `tabindex`: never. It hijacks the tab order for the whole page. Fix the DOM order instead.
|
|
67
|
+
|
|
68
|
+
### Roving tabindex
|
|
69
|
+
|
|
70
|
+
Composite widgets, meaning tabs, menus, toolbars and radio groups, occupy one Tab stop. The active item has `tabindex="0"`, all others `tabindex="-1"`, and arrow keys move both focus and the `0`:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
<div role="tablist">
|
|
74
|
+
{tabs.map((tab, i) => (
|
|
75
|
+
<button
|
|
76
|
+
role="tab"
|
|
77
|
+
tabIndex={i === activeIndex ? 0 : -1}
|
|
78
|
+
aria-selected={i === activeIndex}
|
|
79
|
+
onKeyDown={handleArrowKeys} // ArrowLeft/ArrowRight move activeIndex, wrapping
|
|
80
|
+
>
|
|
81
|
+
{tab.label}
|
|
82
|
+
</button>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Focus trapping and restoration
|
|
88
|
+
|
|
89
|
+
Modals must trap focus. Prefer native `<dialog>` so the browser manages background inertness and initial focus:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
// On open: the browser honors eligible autofocus targets and skips disabled controls.
|
|
93
|
+
dialogRef.current?.showModal();
|
|
94
|
+
|
|
95
|
+
// On close
|
|
96
|
+
dialogRef.current?.close();
|
|
97
|
+
triggerRef.current?.focus(); // return focus to the element that opened it
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
For a custom modal, make the background `inert` and use the project's tested focus-management utility. Prefer an eligible autofocus target, then a visible, enabled focusable control. Skip hidden, disabled and inert candidates. If none exists, focus the dialog container with `tabindex="-1"`. Restore background interactivity when closing.
|
|
101
|
+
|
|
102
|
+
Prefer native `<dialog>` with `showModal()`, which gives you the trap, the `inert` background and Escape handling for free. A custom overlay that can't use it needs `role="dialog"`, `aria-modal="true"` and an accessible name via `aria-labelledby`. Either way:
|
|
103
|
+
|
|
104
|
+
- On open, prefer the eligible autofocus target, then a visible, enabled focusable control; use the container fallback when neither exists. For destructive confirmations, choose the least destructive action.
|
|
105
|
+
- On close, return focus to the trigger, or to the nearest logical container if the trigger is gone.
|
|
106
|
+
- Add `overscroll-behavior: contain` on the dialog so scrolling inside never scrolls the page behind it.
|
|
107
|
+
|
|
108
|
+
## Keyboard patterns (ARIA APG)
|
|
109
|
+
|
|
110
|
+
Native elements come with these behaviors; custom widgets must implement them. A role is a promise. Give something `role="tab"` and users expect the full tab keyboard model.
|
|
111
|
+
|
|
112
|
+
| Widget | Keys |
|
|
113
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
114
|
+
| Dialog | Tab/Shift+Tab cycle inside (wrap at ends); Escape closes |
|
|
115
|
+
| Tabs | Arrow keys move between tabs (wrapping); Tab exits to the panel; Home/End jump to first/last |
|
|
116
|
+
| Menu button | Enter/Space/ArrowDown opens and focuses first item; ArrowUp opens and focuses last; arrows navigate; Escape closes and refocuses the button |
|
|
117
|
+
| Disclosure / accordion | Header is a `<button aria-expanded>`; Enter and Space toggle |
|
|
118
|
+
| Combobox | ArrowDown opens/moves into the list; Enter accepts; Escape closes and returns to the input; typing filters |
|
|
119
|
+
| Listbox / radio group | Arrow keys move selection; one Tab stop for the whole group |
|
|
120
|
+
|
|
121
|
+
Universal rules:
|
|
122
|
+
|
|
123
|
+
- Escape dismisses whatever opened last: tooltip, then menu, then dialog.
|
|
124
|
+
- Arrow keys, not Tab, move within a composite widget; Tab moves between widgets.
|
|
125
|
+
- Tabs choose activation mode: automatic (panel switches on arrow focus) when panels render instantly, manual (Enter/Space to activate) when switching is expensive.
|
|
126
|
+
- Enter submits the focused input's form. In `<textarea>`, Enter inserts a newline and ⌘/Ctrl+Enter submits.
|
|
127
|
+
|
|
128
|
+
## SPA route changes
|
|
129
|
+
|
|
130
|
+
Client-side navigation doesn't reset focus or announce anything. On route change, update `document.title` to match the new context, then move focus to the new view's `<h1>` (given `tabindex="-1"`) or to `<main>`. Restore scroll position on back and forward navigation, and scroll to top on forward navigation.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Forms
|
|
2
|
+
|
|
3
|
+
Labels, autocomplete, error messaging, input types and submit behavior.
|
|
4
|
+
|
|
5
|
+
## Labels
|
|
6
|
+
|
|
7
|
+
Every control needs a programmatic label: `<label for>` pointing at the input's `id`, or a wrapping `<label>`. A placeholder is never a label. It disappears the moment the user types and usually fails contrast.
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<!-- Good: explicit association -->
|
|
11
|
+
<label for="email">Email</label>
|
|
12
|
+
<input id="email" type="email" autocomplete="email" />
|
|
13
|
+
|
|
14
|
+
<!-- Good: wrapping label, so label and control share one hit target -->
|
|
15
|
+
<label> <input type="checkbox" /> Send me updates </label>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Label and control share one hit target, so clicking "Send me updates" toggles the checkbox with no dead zone between them. Mark required fields with native `required` plus a visible indicator explained once per form ("* required").
|
|
19
|
+
|
|
20
|
+
A placeholder used _in addition to_ a label shows an example of the expected format: `placeholder="name@company.com"`.
|
|
21
|
+
|
|
22
|
+
## Error messaging
|
|
23
|
+
|
|
24
|
+
The complete pattern:
|
|
25
|
+
|
|
26
|
+
```html
|
|
27
|
+
<label for="email">Email</label>
|
|
28
|
+
<input
|
|
29
|
+
id="email"
|
|
30
|
+
type="email"
|
|
31
|
+
autocomplete="email"
|
|
32
|
+
aria-invalid="true"
|
|
33
|
+
aria-describedby="email-error"
|
|
34
|
+
/>
|
|
35
|
+
<p id="email-error">Enter a valid email address.</p>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `aria-invalid="true"` on the failing field, removed once fixed.
|
|
39
|
+
- `aria-describedby` links the field to its inline error so screen readers announce it with the field.
|
|
40
|
+
- Errors render inline beside their fields, with an icon or text. Never a red border alone, which is a color-only cue.
|
|
41
|
+
- On submit, focus the first invalid field.
|
|
42
|
+
- Allow incomplete submission so validation can surface. Never disable submit until valid (see below).
|
|
43
|
+
- Accept free text and validate after. Never block typing or filter characters as the user types. Trim values before validating, because autocomplete and text expansion add trailing spaces.
|
|
44
|
+
|
|
45
|
+
## Autocomplete and input types
|
|
46
|
+
|
|
47
|
+
`autocomplete` with a meaningful `name` fills forms in one tap and is a WCAG requirement (1.3.5) for fields about the user. The common tokens:
|
|
48
|
+
|
|
49
|
+
| Field | `autocomplete` |
|
|
50
|
+
| -------------- | ----------------------------------------------------------- |
|
|
51
|
+
| Name | `name` (or `given-name` / `family-name`) |
|
|
52
|
+
| Email | `email` |
|
|
53
|
+
| Phone | `tel` |
|
|
54
|
+
| Address | `street-address`, `address-line1`, `postal-code`, `country` |
|
|
55
|
+
| Card | `cc-number`, `cc-exp`, `cc-csc`, `cc-name` |
|
|
56
|
+
| Login | `username`, `current-password` |
|
|
57
|
+
| Signup / reset | `new-password` |
|
|
58
|
+
| 2FA code | `one-time-code` |
|
|
59
|
+
|
|
60
|
+
Prefix with a section where relevant: `autocomplete="shipping street-address"`.
|
|
61
|
+
|
|
62
|
+
Correct `type` and `inputmode` pick the right mobile keyboard:
|
|
63
|
+
|
|
64
|
+
| Input | Use |
|
|
65
|
+
| ----------------------- | -------------------------------------------------------------------- |
|
|
66
|
+
| Email, URL, phone | `type="email"`, `type="url"`, `type="tel"` |
|
|
67
|
+
| OTP / PIN / card number | `type="text" inputmode="numeric"` (keeps text semantics, no spinner) |
|
|
68
|
+
| Money, decimals | `type="text" inputmode="decimal"` |
|
|
69
|
+
| True numeric quantity | `type="number"` |
|
|
70
|
+
|
|
71
|
+
Disable spellcheck on emails, codes and usernames: `spellcheck="false"`.
|
|
72
|
+
|
|
73
|
+
## Never fight the user's tools
|
|
74
|
+
|
|
75
|
+
- Stay compatible with password managers and 2FA autofill: real `<form>`, correct `autocomplete`, no fake inputs.
|
|
76
|
+
|
|
77
|
+
## Submit behavior
|
|
78
|
+
|
|
79
|
+
- Keep submit enabled until the request starts, then disable it and show a spinner _beside the original label_. "Save" with a spinner, not a bare spinner. The label is what tells assistive tech which button is busy.
|
|
80
|
+
- Announce results. Success goes through a polite live region. On failure, focus the first invalid field, which is itself the announcement. Reserve `role="alert"` for form-level errors not tied to a field ([screen-readers.md](screen-readers.md)).
|
|
81
|
+
- Warn on unsaved changes before navigation, and never lose typed input to a re-render. Hydration must preserve focus and value.
|
|
82
|
+
- Enter submits from any focused input; in `<textarea>`, ⌘/Ctrl+Enter submits.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Hit areas
|
|
2
|
+
|
|
3
|
+
Target sizes, expanding hit areas without changing visual size and collision rules.
|
|
4
|
+
|
|
5
|
+
## Target sizes
|
|
6
|
+
|
|
7
|
+
Separate the conformance baseline from larger usability targets:
|
|
8
|
+
|
|
9
|
+
| Standard | Minimum |
|
|
10
|
+
| ---------------- | ----------------------- |
|
|
11
|
+
| WCAG 2.5.8 (AA) | 24×24px, the hard floor |
|
|
12
|
+
| WCAG 2.5.5 (AAA) | 44×44px |
|
|
13
|
+
| Apple HIG | 44×44pt |
|
|
14
|
+
| Material Design | 48×48dp |
|
|
15
|
+
|
|
16
|
+
WCAG 2.5.8 Level AA requires a 24×24 CSS-pixel target or one of its exceptions. Treat 44px as the recommended touch target for primary controls and 40px as a useful desktop target where density permits. Smaller controls are not automatic failures. Check the spacing, equivalent-control, inline, user-agent and essential exceptions before reporting one.
|
|
17
|
+
|
|
18
|
+
Under the spacing exception, an undersized target passes when a 24px circle centered on its bounding box intersects no other target and no other undersized target's circle. In the simple case, 20px targets need a 4px gap.
|
|
19
|
+
|
|
20
|
+
The visible element can stay small; the hit area is what must be big. Anything that looks clickable must be clickable across its whole visual extent, with no dead zones. A checkbox and its label share one hit target.
|
|
21
|
+
|
|
22
|
+
## Expanding the hit area
|
|
23
|
+
|
|
24
|
+
Where the visible element is smaller, say a 20×20 checkbox, extend the hit area with a pseudo-element. Put it on the wrapping `<label>` or `<button>`, never on the `<input>`, because replaced elements don't render `::before`/`::after` reliably.
|
|
25
|
+
|
|
26
|
+
### CSS example
|
|
27
|
+
|
|
28
|
+
```css
|
|
29
|
+
/* Small checkbox with expanded 44px hit area, on the wrapping label */
|
|
30
|
+
.checkbox-label {
|
|
31
|
+
position: relative;
|
|
32
|
+
width: 20px;
|
|
33
|
+
height: 20px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.checkbox-label::after {
|
|
37
|
+
content: "";
|
|
38
|
+
position: absolute;
|
|
39
|
+
top: 50%;
|
|
40
|
+
left: 50%; /* physical centering: direction-independent */
|
|
41
|
+
transform: translate(-50%, -50%);
|
|
42
|
+
width: 44px;
|
|
43
|
+
height: 44px;
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Tailwind example
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
<button className="relative size-5 after:absolute after:top-1/2 after:left-1/2 after:size-11 after:-translate-1/2">
|
|
51
|
+
<CheckIcon />
|
|
52
|
+
</button>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Layout alternative
|
|
56
|
+
|
|
57
|
+
Where the element can afford real box size, skip the pseudo-element and let the box be the target. That hands the browser real geometry for scrolling and gestures:
|
|
58
|
+
|
|
59
|
+
```css
|
|
60
|
+
.icon-button {
|
|
61
|
+
min-width: 44px;
|
|
62
|
+
min-height: 44px;
|
|
63
|
+
display: inline-grid;
|
|
64
|
+
place-items: center;
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Collision rule
|
|
69
|
+
|
|
70
|
+
Where the extended hit area overlaps another interactive element, shrink the pseudo-element to the largest size that does not collide. Two interactive elements never have overlapping hit areas.
|
|
71
|
+
|
|
72
|
+
## Decorative layers
|
|
73
|
+
|
|
74
|
+
A decorative layer painted over interactive content absorbs every pointer event its box covers: a gradient scrim, a glow, a blurred sheen, a full-bleed `::after`. The control underneath looks live and does nothing, and no hit-area sizing fixes it.
|
|
75
|
+
|
|
76
|
+
Give each one `pointer-events: none` (Tailwind `pointer-events-none`) so events reach the control below, plus `aria-hidden="true"` to keep it out of the accessibility tree:
|
|
77
|
+
|
|
78
|
+
```css
|
|
79
|
+
.card-glow {
|
|
80
|
+
position: absolute;
|
|
81
|
+
inset: 0;
|
|
82
|
+
pointer-events: none;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Keep pointer events on any layer the user is meant to hit: a modal scrim that dismisses on click is a control, not decoration.
|
|
87
|
+
|
|
88
|
+
## Touch behavior
|
|
89
|
+
|
|
90
|
+
- Use `touch-action: manipulation` where removing the mobile tap delay is needed. It permits panning and pinch zoom but disables double-tap zoom on that element; keep the default when that gesture should remain available.
|
|
91
|
+
- Set `touch-action: none` on a surface implementing its own pan, zoom, or drag gestures, so the browser stops claiming them for scrolling and pinch-zoom. Scope it to that surface; at page level it removes scrolling.
|
|
92
|
+
- Set `touch-action: pan-y` on a horizontal carousel or swipe track that handles horizontal movement itself, so the browser keeps vertical page scrolling. A native `scroll-snap-type: x mandatory` track needs neither the gesture code nor the property.
|
|
93
|
+
- Set `-webkit-tap-highlight-color` to match the design instead of the default gray flash.
|
|
94
|
+
- Use `-webkit-touch-callout: none` only when a specific control needs to suppress Safari's long-press callout. Preserve it on links and images where users need native copy, share or link actions, including content links.
|
|
95
|
+
- Put hover-only styling behind `@media (hover: hover)`. On touch, `:hover` latches after a tap and holds until the user taps elsewhere, so it reads as a stuck selected state. Tailwind 4's `hover:` variant already compiles under this query.
|
|
96
|
+
- Prefer generous targets and clear affordances over finicky interactions such as tiny drag handles and precise hover zones.
|
|
97
|
+
|
|
98
|
+
Desktop emulation cannot verify all mobile browser behavior. Check hover, tap behavior, safe areas and keyboard interaction on supported devices, or report those checks as `Not verified`.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Motion and zoom
|
|
2
|
+
|
|
3
|
+
`prefers-reduced-motion`, zoom and reflow, and unit choices that respect user settings.
|
|
4
|
+
|
|
5
|
+
## prefers-reduced-motion
|
|
6
|
+
|
|
7
|
+
Make motion opt-in. Wrap animations in `@media (prefers-reduced-motion: no-preference)` so users who asked for reduced motion get the static version by default, rather than you chasing every animation with an override.
|
|
8
|
+
|
|
9
|
+
```css
|
|
10
|
+
/* Good: motion is opt-in */
|
|
11
|
+
.card {
|
|
12
|
+
/* static styles */
|
|
13
|
+
}
|
|
14
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
15
|
+
.card {
|
|
16
|
+
transition: transform 200ms ease-out;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
// Tailwind: motion-safe / motion-reduce variants
|
|
23
|
+
<div className="motion-safe:transition-transform motion-safe:hover:-translate-y-1" />
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
For an existing codebase where opt-in isn't feasible, a global reset is the fallback. Exclude loading spinners and progress indicators, including animated descendants and pseudo-elements. This example uses `.progress-indicator`; replace it with the project's existing indicator classes:
|
|
27
|
+
|
|
28
|
+
```css
|
|
29
|
+
@media (prefers-reduced-motion: reduce) {
|
|
30
|
+
*:not(.progress-indicator, .progress-indicator *),
|
|
31
|
+
*:not(.progress-indicator, .progress-indicator *)::before,
|
|
32
|
+
*:not(.progress-indicator, .progress-indicator *)::after {
|
|
33
|
+
animation-duration: 0.01ms !important;
|
|
34
|
+
animation-iteration-count: 1 !important;
|
|
35
|
+
transition-duration: 0.01ms !important;
|
|
36
|
+
scroll-behavior: auto !important;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`0.01ms` rather than `none`, so `animationend` and `transitionend` still fire and JS waiting on them doesn't hang.
|
|
42
|
+
|
|
43
|
+
### What to disable vs reduce
|
|
44
|
+
|
|
45
|
+
Reduced motion means reduced, not eliminated. It targets vestibular triggers, not feedback.
|
|
46
|
+
|
|
47
|
+
| Disable entirely | Replace | Keep |
|
|
48
|
+
| ------------------------------------------------ | ------------------------------------------------ | ----------------------------------------------- |
|
|
49
|
+
| Parallax scrolling | Slide/scale/zoom transitions → opacity crossfade | Loading spinners and progress |
|
|
50
|
+
| Autoplaying video, GIFs, looping decoration | Smooth scrolling → instant jump | Instant state changes (hover color, focus ring) |
|
|
51
|
+
| Spinning, large-scale movement across the screen | Auto-rotating carousels → start paused | Brief functional feedback (button press) |
|
|
52
|
+
|
|
53
|
+
Animations are interruptible and driven by user input. Nothing autoplays or refuses to stop, and under reduced motion carousels start paused.
|
|
54
|
+
|
|
55
|
+
## Autoplay and timed UI
|
|
56
|
+
|
|
57
|
+
Motion the user didn't ask for and UI acting on its own schedule:
|
|
58
|
+
|
|
59
|
+
- **No autoplaying media without visible controls** (WCAG 2.2.2). Anything moving, blinking, or updating on its own for more than 5 seconds needs a visible pause or stop control, muted looping hero videos included.
|
|
60
|
+
- **Prefer explicit dismissal over timers.** Auto-dismissal suits low-stakes confirmations and nothing else. A toast carrying an action, an error, or information the user may need stays until dismissed. Where one must time out, 5 seconds is the floor, and hovering or focusing it pauses the timer.
|
|
61
|
+
- **Never put critical information only in a timed element.** A vanished toast with the only link to an undo action is data loss on a schedule.
|
|
62
|
+
|
|
63
|
+
## Zoom and reflow
|
|
64
|
+
|
|
65
|
+
- **200% zoom** (WCAG 1.4.4). All content and functionality survives text scaled to 200%, and the viewport leaves the reader able to zoom.
|
|
66
|
+
- **Reflow at 320px** (WCAG 1.4.10). At 400% zoom on a 1280px viewport, equivalent to a 320px one, the page must work with vertical scrolling alone. Genuinely 2D content is the exception: tables, maps and code blocks scroll inside their own container.
|
|
67
|
+
|
|
68
|
+
Fixed heights are what break under zoom. Use `min-height` on anything containing text and let containers grow.
|
|
69
|
+
|
|
70
|
+
### rem vs px
|
|
71
|
+
|
|
72
|
+
Respect how the codebase is set up. Where the project sizes in `px`, or on an established Tailwind scale, stay consistent and never introduce mixed units into someone else's system. Where you do have the choice, in new code or a codebase already on `rem`, `rem` respects the user's base font size and `px` ignores it:
|
|
73
|
+
|
|
74
|
+
| Use `rem` | Use `px` |
|
|
75
|
+
| ----------------------------------------------------- | ------------------------------ |
|
|
76
|
+
| `font-size` | Borders and hairlines |
|
|
77
|
+
| `max-width` of text containers | Focus outline width and offset |
|
|
78
|
+
| Media-query breakpoints (`@media (min-width: 48rem)`) | `box-shadow` details |
|
|
79
|
+
| Spacing that should scale with text | Fixed-size decorations |
|
|
80
|
+
|
|
81
|
+
Breakpoints are where the choice matters most. At a larger base font size an `em` or `rem` query switches to the mobile layout when the text needs it, and a `px` query does not.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Screen readers
|
|
2
|
+
|
|
3
|
+
Visually hidden content, live regions, toasts, alt text and SVG.
|
|
4
|
+
|
|
5
|
+
## Visually hidden content
|
|
6
|
+
|
|
7
|
+
The canonical `.sr-only` pattern hides content visually while keeping it in the accessibility tree:
|
|
8
|
+
|
|
9
|
+
```css
|
|
10
|
+
.sr-only {
|
|
11
|
+
position: absolute;
|
|
12
|
+
width: 1px;
|
|
13
|
+
height: 1px;
|
|
14
|
+
padding: 0;
|
|
15
|
+
margin: -1px;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
clip: rect(0 0 0 0);
|
|
18
|
+
clip-path: inset(50%);
|
|
19
|
+
white-space: nowrap;
|
|
20
|
+
border: 0;
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Use `1px` boxes, not `0`, because some screen readers skip zero-sized elements. `white-space: nowrap` stops words being read as one run-together string. Never `display: none` or `visibility: hidden`, which remove the content from assistive tech entirely.
|
|
25
|
+
|
|
26
|
+
Tailwind ships this as `sr-only`. Skip links add a focus variant that un-hides it, `focus:not-sr-only` or an override on `:focus`.
|
|
27
|
+
|
|
28
|
+
Use it for context sighted users get visually: `<span class="sr-only">Opens in new tab</span>`, table captions, or an icon-only control's label where `aria-label` isn't an option.
|
|
29
|
+
|
|
30
|
+
## Choosing how to announce a change
|
|
31
|
+
|
|
32
|
+
Work down this list and stop at the first match:
|
|
33
|
+
|
|
34
|
+
1. **Focus moves to named content**, such as a modal with an accessible name and description. Focus exposes that associated content; no duplicate live announcement is needed. For custom field validation, use the next rule even when focus moves to the invalid field.
|
|
35
|
+
2. **Tied to a specific control**, such as a field error or character count: `aria-describedby` on the control, announced with the field.
|
|
36
|
+
3. **Non-urgent, not tied to a control**, such as a toast, "Saved", a result count, or a loading state: a polite live region, `role="status"`.
|
|
37
|
+
4. **Urgent and not tied to a control**, such as a form-level failure or session expiry: `role="alert"`.
|
|
38
|
+
|
|
39
|
+
## Live regions
|
|
40
|
+
|
|
41
|
+
Live regions announce content that changes without a page load: toasts, validation, search-result counts, loading states.
|
|
42
|
+
|
|
43
|
+
| Mechanism | Politeness | Use for |
|
|
44
|
+
| ----------------------------------------------------------------- | ---------------------- | ----------------------------------------------- |
|
|
45
|
+
| `role="status"` (= `aria-live="polite"` + `aria-atomic="true"`) | Waits for a pause | Toasts, "Saved", result counts, loading updates |
|
|
46
|
+
| `role="alert"` (= `aria-live="assertive"` + `aria-atomic="true"`) | Interrupts immediately | Errors and urgent problems only |
|
|
47
|
+
|
|
48
|
+
Rules for reliable announcements:
|
|
49
|
+
|
|
50
|
+
- For repeated polite updates, keep a stable empty region in the DOM before changing its text. Inserting a new polite region with its content is announced inconsistently.
|
|
51
|
+
- Dynamically inserted `role="alert"` content is usually announced, but behavior varies. Use it only for urgent errors not tied to a control, and test the target browser and screen-reader combinations.
|
|
52
|
+
- Default to polite. Overusing `assertive` is the most common live-region mistake, because it interrupts whatever the user was reading.
|
|
53
|
+
- Keep messages short and self-contained. `aria-atomic="true"` re-reads the whole region on change.
|
|
54
|
+
- Never move focus to a toast. Announce it and leave focus where the user is working. Give toasts a generous timeout or a dismiss button, and never put the only path to an action inside an auto-dismissing one.
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
// Region rendered from the start, message injected later
|
|
58
|
+
<div role="status" className="sr-only">
|
|
59
|
+
{statusMessage}
|
|
60
|
+
</div>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For loading states: set `aria-busy="true"` on the updating region, announce "Loading…" politely, then announce the outcome ("Loaded, 12 results").
|
|
64
|
+
|
|
65
|
+
## aria-hidden
|
|
66
|
+
|
|
67
|
+
`aria-hidden="true"` removes an element and its whole subtree from assistive tech. Use it for decorative icons and content duplicated for visual effect. Never put it on or above a focusable element, which creates stops you can Tab to that do not exist for a screen reader. Hiding something interactive means removing it from the tab order too.
|
|
68
|
+
|
|
69
|
+
## Alt text
|
|
70
|
+
|
|
71
|
+
Choose by purpose, not by what the image looks like:
|
|
72
|
+
|
|
73
|
+
| Purpose | Alt | Example |
|
|
74
|
+
| ------------------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------- |
|
|
75
|
+
| Decorative, or redundant with adjacent text | `alt=""` (empty, but present) | Logo next to the company name in text |
|
|
76
|
+
| Informative | Describe the meaning it adds | `alt="Ticket QR code"` |
|
|
77
|
+
| Functional (image is the link/button) | Describe the action or destination | Search icon → `alt="Search"`, not `alt="magnifying glass"` |
|
|
78
|
+
| Image of text | The exact text (better: use real text) | `alt="50% off everything"` |
|
|
79
|
+
| Complex (chart, diagram) | Short summary in `alt`, full data as a table or text nearby | `alt="Revenue by quarter, described below"` |
|
|
80
|
+
|
|
81
|
+
A missing `alt` is worse than an empty one, because screen readers fall back to reading the file name.
|
|
82
|
+
|
|
83
|
+
## SVG
|
|
84
|
+
|
|
85
|
+
- Decorative SVG: `aria-hidden="true"` and `focusable="false"`, the latter for legacy Edge and IE tabbing. No title needed.
|
|
86
|
+
- Meaningful inline SVG: `role="img"` plus `aria-label="…"`, or a `<title>` as the first child referenced by `aria-labelledby`.
|
|
87
|
+
- Simple cases: `<img src="icon.svg" alt="…">` is the most reliable delivery.
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
// Decorative icon inside a labeled button
|
|
91
|
+
<button aria-label="Close">
|
|
92
|
+
<svg aria-hidden="true" focusable="false">…</svg>
|
|
93
|
+
</button>
|
|
94
|
+
|
|
95
|
+
// Standalone meaningful icon
|
|
96
|
+
<svg role="img" aria-label="Verified account">…</svg>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Video and audio
|
|
100
|
+
|
|
101
|
+
Prerecorded video needs captions; provide transcripts for audio. Never autoplay with sound, and always render controls.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Semantics and ARIA
|
|
2
|
+
|
|
3
|
+
Native elements first, landmarks, accessible names and the ARIA rules that keep custom widgets honest.
|
|
4
|
+
|
|
5
|
+
## The rules of ARIA
|
|
6
|
+
|
|
7
|
+
1. If a native HTML element with the semantics and behavior you need exists, use it instead of repurposing another element with ARIA.
|
|
8
|
+
2. Don't change native semantics unless you really have to.
|
|
9
|
+
3. Every interactive ARIA control must be keyboard-operable; a role is a promise of the full keyboard model, states and behavior.
|
|
10
|
+
4. Never put `role="presentation"` or `aria-hidden="true"` on a focusable element.
|
|
11
|
+
5. All interactive elements must have an accessible name.
|
|
12
|
+
|
|
13
|
+
No ARIA is better than bad ARIA. A screen reader trusts your roles, so a wrong one is worse than none.
|
|
14
|
+
|
|
15
|
+
## Button vs link vs div
|
|
16
|
+
|
|
17
|
+
| Element | Use for | Why |
|
|
18
|
+
| --------------- | ----------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
19
|
+
| `<a href>` | Navigation: anything that goes somewhere or changes the URL | Free Cmd/Ctrl/middle-click, right-click → copy link, Enter activation |
|
|
20
|
+
| `<button>` | Actions: submit, toggle, open, delete | Free focus, Enter _and_ Space activation, form semantics |
|
|
21
|
+
| `<div onClick>` | Nothing | No role, no focus, no keyboard; screen readers see plain text |
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
// Bad: invisible to keyboard and screen readers
|
|
25
|
+
<div onClick={openSettings}>Settings</div>
|
|
26
|
+
|
|
27
|
+
// Good: focus, Enter/Space activation and semantics for free
|
|
28
|
+
<button onClick={openSettings}>Settings</button>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
If it looks clickable it must be clickable, and if it's clickable it must be a real interactive element. Rebuilding a link as a button, or the reverse, breaks user expectations. A "button" that navigates is a styled `<a>`.
|
|
32
|
+
|
|
33
|
+
Where a native element is truly impossible, the full polyfill is `role="button"` plus `tabindex="0"` plus Enter and Space handlers, which is why the native element is always less code.
|
|
34
|
+
|
|
35
|
+
## Landmarks and headings
|
|
36
|
+
|
|
37
|
+
- Expose one visible primary `<main>` landmark. `<header>`, `<nav>`, `<aside>`, `<footer>` map to landmarks screen-reader users jump between.
|
|
38
|
+
- Multiple landmarks of the same type need distinguishing labels: `<nav aria-label="Primary">`, `<nav aria-label="Breadcrumbs">`.
|
|
39
|
+
- Do not report the one-`<h1>` or the no-skipped-levels convention as a standalone WCAG failure without a concrete navigation or comprehension impact. Headings are structure, not styling; style a heading level with CSS instead of picking the tag by size.
|
|
40
|
+
- `<title>` matches the current context, most specific first: `Billing · Settings · Acme`.
|
|
41
|
+
|
|
42
|
+
## Accessible names
|
|
43
|
+
|
|
44
|
+
Name precedence: `aria-labelledby` > `aria-label` > native label (`<label>`, text content, `alt`) > `title` attribute.
|
|
45
|
+
|
|
46
|
+
- Prefer visible text or `aria-labelledby` over `aria-label`, which is invisible, drifts out of sync with the UI and is handled inconsistently by translation tools.
|
|
47
|
+
- Icon-only buttons always need a name: `<button aria-label="Close">` with the icon `aria-hidden="true"`.
|
|
48
|
+
- WCAG 2.5.3 Label in Name: a button showing "Send" with `aria-label="Submit message"` breaks voice control users who say "click Send".
|
|
49
|
+
- Accessible names must exist even when the design omits visible labels.
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// Good: name from visible text, icon hidden
|
|
53
|
+
<button>
|
|
54
|
+
<TrashIcon aria-hidden="true" /> Delete
|
|
55
|
+
</button>
|
|
56
|
+
|
|
57
|
+
// Good: icon-only, explicit name
|
|
58
|
+
<button aria-label="Delete">
|
|
59
|
+
<TrashIcon aria-hidden="true" />
|
|
60
|
+
</button>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Add `translate="no"` to brand names, code tokens and identifiers so auto-translation doesn't garble them.
|
|
64
|
+
|
|
65
|
+
## Common ARIA mistakes
|
|
66
|
+
|
|
67
|
+
| Mistake | Why it fails |
|
|
68
|
+
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
69
|
+
| `aria-label` on a plain `<div>` or `<span>` | Most screen readers ignore names on non-interactive, role-less elements |
|
|
70
|
+
| `<button role="button">` | Redundant role; adds noise, no benefit |
|
|
71
|
+
| `aria-hidden="true"` on or above a focusable element | Creates elements you can Tab to but that don't exist for screen readers |
|
|
72
|
+
| `aria-labelledby`/`aria-describedby` pointing at a missing ID | Silently produces no name or description |
|
|
73
|
+
| `role="menu"` on a nav list | `menu` promises app-style arrow-key behavior; site navigation is `<nav>` with a list |
|
|
74
|
+
|
|
75
|
+
## Disabled states
|
|
76
|
+
|
|
77
|
+
Native `disabled` supplies the platform's complete disabled behavior. It removes the control from the tab order, suppresses activation, applies `:disabled` and excludes form controls from submission. Use it when a native control is genuinely unavailable. `aria-disabled="true"` only announces the state, changing neither focusability, nor behavior, nor styling.
|
|
78
|
+
|
|
79
|
+
- Never disable submit buttons. Keep them enabled, validate on submit and focus the first error ([forms.md](forms.md)).
|
|
80
|
+
- A natively `disabled` control suppresses pointer events and leaves the tab order, so a tooltip on it never opens for keyboard or touch users and is unreliable for mouse users. Put the reason in visible text beside the control, or switch to `aria-disabled="true"`, which keeps it focusable and hoverable and can carry a tooltip.
|
|
81
|
+
- Use `aria-disabled="true"` where keeping a control discoverable in the tab order is intentional, or where a custom control cannot use native `disabled`.
|
|
82
|
+
- With `aria-disabled="true"`, block pointer and keyboard activation in the handler, prevent form submission where applicable, add explicit styling including forced-colors support and explain nearby why the action is unavailable.
|
|
83
|
+
- Never set both `disabled` and `aria-disabled` on the same element.
|
|
84
|
+
- Disabled controls are exempt from contrast minimums. Keep them legible anyway.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jakub Krehel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Source notices
|
|
2
|
+
|
|
3
|
+
This notice accompanies the hr-better-colors bundle when copied on its own.
|
|
4
|
+
|
|
5
|
+
## jakubkrehel/skills
|
|
6
|
+
|
|
7
|
+
Source: https://github.com/jakubkrehel/skills (compared at revision `267330e1adfc66a718fb65fa6918c1f06d0a689e`)
|
|
8
|
+
|
|
9
|
+
- `skills/hr-better-accessibility/`, `skills/hr-better-colors/`, `skills/hr-better-layout/`,
|
|
10
|
+
`skills/hr-better-typography/` and `skills/hr-better-writing/`: adapted skill bundles,
|
|
11
|
+
including their supporting references and discovery metadata.
|
|
12
|
+
|
|
13
|
+
Copyright (c) 2026 Jakub Krehel
|
|
14
|
+
|
|
15
|
+
## house-rules and MIT permission notice
|
|
16
|
+
|
|
17
|
+
MIT License
|
|
18
|
+
|
|
19
|
+
Copyright (c) 2026 Aaron Queen
|
|
20
|
+
|
|
21
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
22
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
23
|
+
in the Software without restriction, including without limitation the rights
|
|
24
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
25
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
26
|
+
furnished to do so, subject to the following conditions:
|
|
27
|
+
|
|
28
|
+
The above copyright notice and this permission notice shall be included in all
|
|
29
|
+
copies or substantial portions of the Software.
|
|
30
|
+
|
|
31
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
32
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
33
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
34
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
35
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
36
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
37
|
+
SOFTWARE.
|