Accessibility Patterns for Svelte
Why accessibility matters
Accessibility (a11y) isn't a checkbox for compliance — it's about whether your product actually works for real people. Around 15% of the world's population has some form of disability. In the US, about 7 million people use screen readers. When you build a form with no label associations, or a clickable <div> with no keyboard support, those users are locked out of your feature.
More practically: accessible HTML is better HTML for everyone. Screen reader users aren't the only ones affected:
- Users navigating by keyboard (power users, people with motor impairments) need focusable elements and keyboard handlers
- Users on slow networks or with JavaScript disabled need semantic HTML that works without CSS
- Search engines read semantic HTML the same way screen readers do
Svelte's a11y warnings are the compiler catching real issues — they're worth fixing properly, not suppressing.
a11y_no_static_element_interactions — interactive div needs a role
What's happening
Svelte warns when a <div> (or other non-interactive element) has event handlers like onclick, ondrop, or ondragover but no ARIA role. Assistive technologies identify elements by their role. A <div> with no role is announced as a "generic container" — screen reader users won't know it's interactive.
The fix
Add a semantic role that matches what the element does:
For a drop zone:
<div
role="region"
aria-label="File drop zone"
ondrop={handleDrop}
ondragover={handleDragover}
>
Drop files here
</div>For a clickable control that isn't a button:
<div
role="button"
tabindex="0"
onclick={handleClick}
onkeydown={(e) => e.key === 'Enter' && handleClick()}
>
Click me
</div>Why the onkeydown handler? <div> elements don't receive keyboard events by default — they're not in the tab order unless you add tabindex. Without onkeydown, keyboard users who tab to the element and press Enter will get nothing. The handler makes the element behave like a real button for keyboard navigation.
Prefer <button> over role="button": If the element's purpose is "the user clicks this to do something", just use <button>. It gets keyboard support, focus styling, and role announcement for free. Only use role="button" on a non-button element when you genuinely can't use a <button>.
a11y_label_has_associated_control — label must be linked to its input
What's happening
A <label> with no programmatic link to its <input> is useless for screen readers. When the user focuses the input, the screen reader announces the input type and any visible label, but only if the label is programmatically associated. Without it: "text field, blank" with no context.
The fix
Option A — for/id pair (prefer when the label and input are far apart in the DOM):
<label for="post-title">Title</label>
<input id="post-title" bind:value={title} />When the user focuses the input, the screen reader announces: "Title, text field".
Option B — wrap the input inside the label (no ID needed, avoids duplicate-ID bugs when a component is rendered multiple times):
<label>
Title
<input bind:value={title} />
</label>The wrapping approach is often easier in components because you don't need to manage unique IDs. If you use Option A and the component is rendered twice on the same page, you'd have duplicate IDs — which breaks the association.
a11y_autofocus — use programmatic focus instead
What's happening
The autofocus HTML attribute causes a browser to automatically focus an element when the page loads. Svelte warns against it because it can disorient screen reader users: the virtual cursor moves unexpectedly before they've had a chance to read the page context.
The fix
If focus-on-mount is genuinely needed (a modal dialog's primary action, a search field that is the sole purpose of the page), use a bound element reference and $effect:
<script lang="ts">
let inputEl: HTMLInputElement | undefined = $state();
$effect(() => {
inputEl?.focus();
});
</script>
<input bind:this={inputEl} bind:value={val} />Why $effect instead of autofocus? $effect runs after the component mounts, giving you explicit control. You can conditionally focus (only when a modal opens, not on every re-render), and you can add cleanup. The autofocus attribute fires immediately during HTML parsing, before the full page context is ready, which breaks screen reader virtual cursor positioning.
If focus on mount isn't genuinely needed, remove the autofocus attribute entirely. Auto-focus is disruptive — only use it when the focused element is the primary reason the user navigated to this state.
a11y_click_events_have_key_events — click events need keyboard equivalents
What's happening
An interactive element with onclick but no keyboard handler (onkeydown, onkeyup, onkeypress) is only usable with a mouse. Keyboard users — including many users with motor impairments and all screen reader users in virtual browse mode — can't trigger it.
The fix
Add a keyboard handler that responds to Enter (for "activate") and sometimes Space (for toggle controls):
<div
role="button"
tabindex="0"
onclick={handleActivate}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault(); // prevent Space from scrolling the page
handleActivate();
}
}}
>
Activate
</div>Again: if you're implementing a clickable control, prefer <button> which gets all of this for free.
a11y_img_redundant_alt — don't say "image" or "photo" in alt text
What's happening
Screen readers already announce images as images. Alt text saying "photo of a cat" causes the screen reader to say "image, photo of a cat" — redundant and slightly annoying.
The fix
Write alt text that describes the content or purpose, not the format:
<!-- Wrong: redundant -->
<img src={cat.jpg} alt="image of a cat sitting on a chair" />
<!-- Correct: describes the content -->
<img src={cat.jpg} alt="A cat sitting on a wooden chair by a window" />
<!-- Decorative image (no meaningful content): empty alt -->
<img src={divider.png} alt="" role="presentation" />For decorative images that add no information, use alt="" and role="presentation" to tell screen readers to skip the element entirely.
General principle
When Svelte's a11y lint fires, ask: "what is a screen reader user's experience of this element?" Usually the fix is straightforward — a role, a label, a keyboard handler. The goal isn't to silence the warning; it's to make the feature work for everyone.