|
| 1 | +# Accessibility (a11y) Patterns — Detailed Reference |
| 2 | + |
| 3 | +Full reference for fixing Svelte a11y build warnings. For the quick cheat sheet loaded in project context, see `a11y-patterns.md`. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## Rule 1: Always Associate Labels with Inputs |
| 8 | + |
| 9 | +Every `<label>` must be linked to its control. There are two valid patterns: |
| 10 | + |
| 11 | +```svelte |
| 12 | +<!-- ✅ CORRECT: for= points to input's id --> |
| 13 | +<label for="email">Email</label> |
| 14 | +<input id="email" type="email" bind:value={email} /> |
| 15 | +
|
| 16 | +<!-- ✅ CORRECT: label wraps the input --> |
| 17 | +<label> |
| 18 | + Email |
| 19 | + <input type="email" bind:value={email} /> |
| 20 | +</label> |
| 21 | +
|
| 22 | +<!-- ❌ WRONG: label has no association --> |
| 23 | +<label>Email</label> |
| 24 | +<input type="email" bind:value={email} /> |
| 25 | +
|
| 26 | +<!-- ❌ WRONG: label has for= but input has no matching id --> |
| 27 | +<label for="email">Email</label> |
| 28 | +<input type="email" bind:value={email} /> |
| 29 | +``` |
| 30 | + |
| 31 | +**DaisyUI form fields** — `<label class="label">` is a layout wrapper, not a semantic label. It still needs `for=`: |
| 32 | +```svelte |
| 33 | +<!-- ✅ DaisyUI pattern --> |
| 34 | +<label class="label" for="phone"> |
| 35 | + <span class="label-text">Phone</span> |
| 36 | +</label> |
| 37 | +<input id="phone" class="input" type="tel" bind:value={phone} /> |
| 38 | +
|
| 39 | +<!-- ✅ DaisyUI checkbox/toggle — label wraps the input --> |
| 40 | +<label class="flex items-center gap-2" for="active"> |
| 41 | + <input id="active" type="checkbox" class="checkbox" bind:checked={active} /> |
| 42 | + <span>Active</span> |
| 43 | +</label> |
| 44 | +``` |
| 45 | + |
| 46 | +--- |
| 47 | + |
| 48 | +## Rule 2: Never Use `<div>` for Click Interactions |
| 49 | + |
| 50 | +Clickable `<div>` elements are invisible to keyboard users and screen readers. Always use semantic elements. |
| 51 | + |
| 52 | +```svelte |
| 53 | +<!-- ✅ CORRECT: use <button> for actions --> |
| 54 | +<button type="button" onclick={() => select(item)}> |
| 55 | + {item.name} |
| 56 | +</button> |
| 57 | +
|
| 58 | +<!-- ✅ CORRECT: use <a> for navigation --> |
| 59 | +<a href="/jobs/{id}">{title}</a> |
| 60 | +
|
| 61 | +<!-- ❌ WRONG: div with onclick --> |
| 62 | +<div onclick={() => select(item)}>{item.name}</div> |
| 63 | +
|
| 64 | +<!-- ❌ WRONG: span with onclick --> |
| 65 | +<span onclick={handleClick}>Click me</span> |
| 66 | +``` |
| 67 | + |
| 68 | +**If you must use a non-button element** (e.g., a card that's both a link and has internal actions), add role + tabindex + keyboard handler: |
| 69 | +```svelte |
| 70 | +<!-- ✅ Only when <button> truly won't work --> |
| 71 | +<div |
| 72 | + role="button" |
| 73 | + tabindex="0" |
| 74 | + onclick={handleClick} |
| 75 | + onkeydown={(e) => e.key === 'Enter' && handleClick()} |
| 76 | +> |
| 77 | + ... |
| 78 | +</div> |
| 79 | +``` |
| 80 | + |
| 81 | +--- |
| 82 | + |
| 83 | +## Rule 3: Give Every Input an Accessible Name |
| 84 | + |
| 85 | +Inputs without visible labels need `aria-label` or `aria-labelledby`. |
| 86 | + |
| 87 | +```svelte |
| 88 | +<!-- ✅ Icon-only search input --> |
| 89 | +<input type="search" aria-label="Search jobs" bind:value={query} /> |
| 90 | +
|
| 91 | +<!-- ✅ Label provided by nearby heading --> |
| 92 | +<h2 id="filters-heading">Filters</h2> |
| 93 | +<input aria-labelledby="filters-heading" type="text" /> |
| 94 | +
|
| 95 | +<!-- ✅ Placeholder is NOT a label — still need aria-label --> |
| 96 | +<input |
| 97 | + type="text" |
| 98 | + placeholder="Search..." |
| 99 | + aria-label="Search" |
| 100 | + bind:value={query} |
| 101 | +/> |
| 102 | +
|
| 103 | +<!-- ❌ WRONG: no label, no aria-label --> |
| 104 | +<input type="text" placeholder="Search..." bind:value={query} /> |
| 105 | +``` |
| 106 | + |
| 107 | +--- |
| 108 | + |
| 109 | +## Rule 4: Interactive ARIA Roles Need tabindex |
| 110 | + |
| 111 | +When you assign an interactive ARIA role, the element must be focusable. |
| 112 | + |
| 113 | +```svelte |
| 114 | +<!-- ✅ CORRECT --> |
| 115 | +<div role="menu" tabindex="0">...</div> |
| 116 | +<div role="dialog" tabindex="-1">...</div> |
| 117 | +<div role="button" tabindex="0" onclick={...} onkeydown={...}>...</div> |
| 118 | +
|
| 119 | +<!-- ❌ WRONG: role without tabindex --> |
| 120 | +<div role="menu">...</div> |
| 121 | +``` |
| 122 | + |
| 123 | +**tabindex values:** |
| 124 | +- `tabindex="0"` — in tab order (user can tab to it) |
| 125 | +- `tabindex="-1"` — focusable by script, not in tab order (good for modals) |
| 126 | + |
| 127 | +--- |
| 128 | + |
| 129 | +## Rule 5: Image Alt Text Rules |
| 130 | + |
| 131 | +```svelte |
| 132 | +<!-- ✅ Descriptive alt for informational images --> |
| 133 | +<img src={avatar} alt="Profile photo of {name}" /> |
| 134 | +
|
| 135 | +<!-- ✅ Empty alt for decorative images (screen reader skips it) --> |
| 136 | +<img src={decorative} alt="" /> |
| 137 | +
|
| 138 | +<!-- ❌ WRONG: redundant words — screen readers say "image" automatically --> |
| 139 | +<img src={photo} alt="image of the job site" /> |
| 140 | +<img src={photo} alt="photo of technician" /> |
| 141 | +<img src={photo} alt="picture showing invoice" /> |
| 142 | +``` |
| 143 | + |
| 144 | +--- |
| 145 | + |
| 146 | +## Rule 6: Video Elements Need Caption Tracks |
| 147 | + |
| 148 | +```svelte |
| 149 | +<!-- ✅ With real captions --> |
| 150 | +<video src={url} controls> |
| 151 | + <track kind="captions" src={captionsUrl} srclang="en" label="English" /> |
| 152 | +</video> |
| 153 | +
|
| 154 | +<!-- ✅ Placeholder when captions unavailable (suppresses warning) --> |
| 155 | +<video src={url} controls> |
| 156 | + <track kind="captions" src="" srclang="en" label="English" default /> |
| 157 | +</video> |
| 158 | +``` |
| 159 | + |
| 160 | +--- |
| 161 | + |
| 162 | +## Quick Reference: Warning → Fix |
| 163 | + |
| 164 | +| Svelte Warning | Fix | |
| 165 | +|----------------|-----| |
| 166 | +| `a11y_label_has_associated_control` | Add `for=` to `<label>` matching `id=` on input, or wrap input inside label | |
| 167 | +| `a11y_click_events_have_key_events` | Replace `<div onclick>` with `<button type="button">` | |
| 168 | +| `a11y_no_static_element_interactions` | Replace `<div onclick>` with `<button type="button">` | |
| 169 | +| `a11y_consider_explicit_label` | Add `aria-label` or `<label>` to input | |
| 170 | +| `a11y_no_noninteractive_element_interactions` | Use semantic element (`<button>`, `<a>`) or add proper role | |
| 171 | +| `a11y_no_noninteractive_tabindex` | Remove `tabindex` from non-interactive elements, or give them an interactive role | |
| 172 | +| `a11y_interactive_supports_focus` | Add `tabindex="0"` to element with interactive ARIA role | |
| 173 | +| `a11y_media_has_caption` | Add `<track kind="captions">` inside `<video>` | |
| 174 | +| `a11y_img_redundant_alt` | Remove "image", "photo", "picture" from alt text | |
| 175 | +| `a11y_role_supports_aria_props` | Remove ARIA attributes invalid for the element's role | |
0 commit comments