Accessibility isn't an afterthought—it's a first-class concern in Svelte, baked into the compiler itself. When we build for production applications serving thousands of users, we quickly learn that accessible interfaces aren't nice-to-haves; they're requirements. Accessibility in Svelte: real-world lessons and examples shows us that the framework actively helps you catch and prevent common issues before they reach your users.
Compiler warnings—Svelte warns about common accessibility issues
One of Svelte's strongest features is that accessibility violations often trigger compiler warnings. This is rare among frontend frameworks. When you write something semantically questionable, the compiler flags it immediately in your development process.
For example, if you create a button using a <div> with a click handler, Svelte warns you:
<!-- This triggers a compiler warning -->
<div on:click={() => performAction()}>
Click me
</div>The warning message nudges you toward the proper semantic element:
<!-- Correct approach -->
<button on:click={() => performAction()}>
Click me
</button>We've seen this in production. A junior engineer on our team built an interactive component with divs styled as buttons. The compiler caught it during review, and we fixed it in minutes rather than discovering it in accessibility testing weeks later. The same applies to images without alt text, labels without associated form inputs, and missing ARIA attributes on interactive elements.
Svelte's compiler runs through a11y checks by default. You can configure strictness in your svelte.config.js:
const config = {
compilerOptions: {
dev: true,
immutable: false,
legacy: false,
accessibilityChecks: true,
hydratable: false,
customElement: false,
preserveComments: false,
preserveWhitespace: false
}
};The compiler checks for:
- Missing or empty alt text on images
- Interactive elements that should be buttons or links
- Form inputs without associated labels
- Missing ARIA attributes on custom widgets
- Redundant ARIA attributes
- Event handlers on non-interactive elements
These warnings are noise-free. They appear when there's genuine risk, not for every possible edge case.
Semantic HTML—Buttons for actions, links for navigation
The foundation of accessibility is semantic HTML. A button triggers an action within the page. A link navigates to a new page or URL. The distinction matters because screen readers, browsers, and assistive technologies all rely on these semantic meanings.
We learned this through a real incident. Our e-commerce platform had navigation implemented as clickable divs styled with CSS to look like links. Mobile users with screen readers couldn't navigate. Users navigating via keyboard found no way to move through the site. We refactored it in a weekend.
The correct approach is straightforward:
<!-- Navigation: use links -->
<nav>
<a href="/products">Products</a>
<a href="/cart">Cart</a>
<a href="/account">Account</a>
</nav>
<!-- Actions: use buttons -->
<button on:click={() => addToCart(product)}>
Add to Cart
</button>
<button on:click={() => toggleMenu()}>
Menu
</button>In SvelteKit, use the <a> tag for client-side navigation. The router handles the page transition seamlessly:
<!-- In a SvelteKit component -->
<a href="/dashboard">Go to Dashboard</a>
<!-- This works with the built-in router -->
<a href="/products?category=electronics">Electronics</a>For form submissions, use <form> elements with proper submit buttons:
<form on:submit|preventDefault={handleSubmit}>
<label for="email">Email</label>
<input id="email" type="email" bind:value={email} required />
<label for="password">Password</label>
<input id="password" type="password" bind:value={password} required />
<button type="submit">Sign In</button>
</form>Notice the for attribute on labels and matching id on inputs. This creates an association that screen readers announce and that expands the click target for mobile users.
Focus management—Move focus after navigation and dialogs
When a page loads or a user navigates, their focus point should move to the new content. Without this, a keyboard user might be stuck tabbing through a header that's no longer relevant. We discovered this when testing our travel platform's search results page.
After a user searches, the results load, but their focus stayed in the search input. A screen reader user heard no indication of the new content. The fix involves programmatic focus management:
<script>
import { onMount } from 'svelte';
let resultsContainer;
let searchQuery = '';
let results = [];
async function handleSearch() {
const data = await fetch(`/api/search?q=${searchQuery}`);
results = await data.json();
// Move focus to results
if (resultsContainer) {
resultsContainer.focus();
}
}
onMount(() => {
// Set focus on page load if needed
if (results.length > 0) {
resultsContainer.focus();
}
});
</script>
<div>
<input
type="search"
bind:value={searchQuery}
on:keydown={(e) => e.key === 'Enter' && handleSearch()}
placeholder="Search trips..."
/>
<button on:click={handleSearch}>Search</button>
</div>
<div
bind:this={resultsContainer}
tabindex="-1"
role="region"
aria-live="polite"
aria-label="Search results"
>
{#each results as result (result.id)}
<article>
<h3>{result.title}</h3>
<p>{result.description}</p>
</article>
{/each}
</div>Dialog or modal windows require similar care. When a modal opens, focus should move inside it. When it closes, focus should return to the button that opened it:
<script>
let showModal = false;
let triggerButton;
function openModal() {
showModal = true;
}
function closeModal() {
showModal = false;
// Return focus to the button that opened the modal
if (triggerButton) {
triggerButton.focus();
}
}
</script>
<button bind:this={triggerButton} on:click={openModal}>
Open Dialog
</button>
{#if showModal}
<div class="modal-overlay" on:click={closeModal}>
<div
class="modal-content"
on:click|stopPropagation
role="dialog"
aria-modal="true"
aria-label="Confirmation dialog"
>
<h2>Confirm Action</h2>
<p>Are you sure?</p>
<button on:click={closeModal}>Cancel</button>
<button on:click={() => { handleConfirm(); closeModal(); }}>
Confirm
</button>
</div>
</div>
{/if>The aria-modal attribute tells assistive technology that this is a modal dialog, and focus shouldn't move outside of it while it's open.
Test—Screen readers and keyboard-only use
Compiler warnings and semantic markup are foundations. Testing with real assistive technologies is non-negotiable. We use two primary methods in our testing workflow:
Screen reader testing: Install a free screen reader like NVDA (Windows) or VoiceOver (macOS, iOS). Navigate your app using only the screen reader. You'll immediately hear where information is missing or unclear. Our CRM team spent an afternoon with NVDA and found that customer names in tables weren't announced clearly because we'd skipped proper table headers.
Keyboard-only navigation: Unplug your mouse. Navigate entirely with Tab, Shift+Tab, Enter, and arrow keys. Every interactive element should be reachable and operable. If you can't complete a user flow with just your keyboard, it's not accessible. We caught this in our SaaS admin panel where complex data tables required mouse selection.
Automated testing tools like axe DevTools, Lighthouse, and WebAIM are helpful for catching baseline issues, but they catch maybe 30-40% of real accessibility problems. Manual testing is where the gap closes.
In your test suite, include keyboard navigation checks:
import { render, screen } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import SearchComponent from './Search.svelte';
test('navigation via keyboard', async () => {
const user = userEvent.setup();
render(SearchComponent);
// Tab to the search input
await user.tab();
const input = screen.getByRole('searchbox');
expect(input).toHaveFocus();
// Type and submit
await user.keyboard('flights');
await user.keyboard('{Enter}');
// Focus should move to results
const results = screen.getByRole('region', { name: /search results/i });
expect(results).toHaveFocus();
});Common mistakes
Ignoring Svelte compiler warnings: Treat them like any other compiler error. Don't suppress them unless you have a documented reason. We've seen teams disable a11y checks to "speed up development," only to spend weeks fixing issues later.
Using role attributes incorrectly: role="button" on a div doesn't make it a button. Use actual buttons or provide full keyboard support and event handling if you truly need a custom widget. This is a common trap in our design-heavy projects.
Forgetting about color contrast: Svelte won't warn you, but your users will suffer. Text should meet WCAG AA standards (4.5:1 for normal text). Test your color combinations with tools like WebAIM's contrast checker.
Setting tabindex="0" on everything: Most elements don't need it. Focus should flow naturally through the DOM order. Only use tabindex when the DOM order doesn't match the visual or logical flow.
Key takeaways
- Svelte's compiler catches many common accessibility issues automatically—pay attention to those warnings.
- Use semantic HTML: buttons for actions, links for navigation, forms for input. This is the bedrock.
- Manage focus explicitly after navigation, modal dialogs, and dynamic content updates.
- Test with real screen readers and keyboard-only navigation. Automated tools are helpers, not replacements.
- Accessibility is a production requirement, not a nice-to-have. Build it in from the start.






Comments
Loading comments…