SvelteKit Accessibility Checklist 2026 | WCAG 2.1 AA & EAA Compliance
Last updated: 2026-05-24
SvelteKit is unusual among JavaScript frameworks because it ships real accessibility features by default - and the most common SvelteKit accessibility problems come from developers unknowingly breaking those defaults. After hydration, SvelteKit behaves like a single-page app: client-side navigation swaps content without a full page reload, which normally leaves screen reader users unaware that the page changed and leaves keyboard focus stranded on the old link. SvelteKit handles both for you: it injects a visually hidden live region that announces each new page's title on navigation, and after navigating it moves focus to the document body so the next Tab starts at the top of the new page. The Svelte compiler also emits accessibility warnings at build time for issues like images without alt text, click handlers on non-interactive elements without keyboard handlers, and form labels not associated with a control. These are genuine advantages, but they depend on the developer cooperating: the route announcer only works if every route sets a unique, descriptive
Common Accessibility Issues
SvelteKit's built-in route announcer reads the new page's document title aloud after client-side navigation, which is how screen reader users learn the page changed. If a route has no <title> in its <svelte:head>, or every route shares the same generic title, the announcer says nothing useful and the navigation is silent for screen reader users. A missing title also fails Page Titled outright.
Give every route a unique, descriptive <title> set with <svelte:head>, ideally driven by the page's data so it reflects the actual content (for example the article name on an article route). Establish a consistent title pattern like 'Page name - Site name'. Because the route announcer depends on the title, treat a unique title as a per-route requirement, not an afterthought, and verify with a screen reader that navigating between routes announces the new page.
<!-- +layout.svelte sets one title for the whole app; routes set none -->
<svelte:head><title>My App</title></svelte:head> <!-- +page.svelte for each route -->
<script>export let data;</script>
<svelte:head>
<title>{data.article.title} - My App</title>
</svelte:head> Using on:click on a <div> or <span> instead of an <a> or <button> produces an element that a mouse can use but a keyboard cannot focus or activate, and that a screen reader does not announce as interactive. In SvelteKit this also bypasses the framework's client-side navigation, since real internal links are what trigger the route announcer and focus management. The Svelte compiler warns about this (a11y-click-events-have-key-events, a11y-no-static-element-interactions), but the warnings are easy to ignore.
Use a real <a href> for navigation (which SvelteKit enhances into client-side navigation automatically) and a real <button> for actions. Reserve click handlers on generic elements for genuinely non-interactive enhancements. If you must make a custom control, give it the correct role, make it focusable with tabindex=0, and handle Enter/Space - but a native element is almost always the better answer and keeps SvelteKit's navigation working.
<div class="nav-link" on:click={() => goto('/pricing')}>Pricing</div>
<span class="btn" on:click={save}>Save</span> <a class="nav-link" href="/pricing">Pricing</a>
<button class="btn" on:click={save}>Save</button> The Svelte compiler flags real accessibility problems at build time - images without alt, labels not associated with a control, click handlers without keyboard handlers, invalid ARIA, autofocus, redundant roles. Teams under deadline pressure often blanket-suppress these with <!-- svelte-ignore --> comments or by turning warnings off, which removes one of SvelteKit's biggest accessibility advantages and lets the underlying failures ship.
Treat the compiler's a11y warnings as build errors to fix, not noise to silence. Add alt text (empty alt for decorative images), associate every label with its control, give interactive elements keyboard handlers, and correct ARIA usage. Run svelte-check in CI so a11y warnings are visible on every pull request. Reserve svelte-ignore for the rare, documented false positive, with a comment explaining why - never as a default.
<!-- svelte-ignore a11y-missing-attribute -->
<img src={product.image}>
<!-- svelte-ignore a11y-label-has-associated-control -->
<label>Email</label><input type="email"> <img src={product.image} alt={product.name}>
<label for="email">Email</label>
<input id="email" type="email"> SvelteKit form actions with use:enhance let forms submit and update without a full reload, which is good for usability but means validation errors and success messages are injected into the DOM without a page change. If errors are not tied to their fields and results are not announced, a screen reader user submits the form and hears nothing - they do not know whether it failed, which field is wrong, or whether it succeeded.
Render server-side validation errors returned from the action next to each field, associated with aria-describedby, and set aria-invalid on the failing input. Move focus to the first invalid field, or to an error summary, after a failed submit. Put success and error messages in an aria-live region (polite for success, assertive only for urgent failures) so the result of an enhanced submission is announced. Keep the form usable without JavaScript, since use:enhance progressively enhances a working HTML form.
<form method="POST" use:enhance>
<input name="email" type="email">
{#if form?.error}<p>{form.error}</p>{/if}
</form> <form method="POST" use:enhance>
<label for="email">Email</label>
<input id="email" name="email" type="email"
aria-invalid={form?.errors?.email ? 'true' : undefined}
aria-describedby={form?.errors?.email ? 'email-err' : undefined}>
{#if form?.errors?.email}<span id="email-err">{form.errors.email}</span>{/if}
<p aria-live="polite">{form?.success ? 'Message sent.' : ''}</p>
</form> SvelteKit manages focus on route navigation, but not for in-page UI you build yourself. Dialogs, drawers, and {#if}-revealed panels commonly open without moving focus into them, without trapping focus while open, and without returning focus to the trigger on close. Keyboard users get stranded behind the overlay, and screen reader users are not told the dialog opened. The same gap applies to content revealed by user action that appears far from the control that triggered it.
Use the native <dialog> element where possible - it provides focus trapping, Escape-to-close, and screen reader semantics with little code. For custom overlays, move focus to the dialog (its heading or first control) when it opens, trap Tab within it while open, restore focus to the trigger on close, and mark the dialog with role=dialog and aria-modal=true and a label. For revealed inline content, move focus to it or announce it so it is not missed.
{#if open}
<div class="modal">
<h2>Confirm</h2>
<button on:click={() => open = false}>Close</button>
</div>
{/if} <dialog bind:this={dialog} aria-labelledby="confirm-title">
<h2 id="confirm-title">Confirm</h2>
<button on:click={() => dialog.close()}>Close</button>
</dialog>
<!-- open with dialog.showModal(); focus + Escape handled by the browser --> SvelteKit's load functions and client-side data fetching make it easy to show spinners and skeletons while content loads, and to swap content in place after an action. Sighted users see the spinner and the updated content; screen reader users get no announcement that loading started, finished, or that new results appeared. This is especially common on search-as-you-type, filtered lists, and 'load more' patterns.
Wrap status text in an aria-live="polite" region (for example 'Loading results' then '12 results found') so changes are announced without moving focus. For a spinner, give it an accessible label or pair it with visually hidden live-region text rather than relying on the animation alone. When results update after a filter or 'load more', announce the new count, and make sure newly loaded focusable content does not steal or lose focus unexpectedly.
{#if loading}<div class="spinner"></div>{/if}
{#each results as r}<Card {r} />{/each} <div aria-live="polite" class="sr-only">
{loading ? 'Loading results' : `${results.length} results found`}
</div>
{#if loading}<div class="spinner" role="img" aria-label="Loading"></div>{/if}
{#each results as r}<Card {r} />{/each} Because SvelteKit's +layout.svelte wraps every route, an inaccessible root layout multiplies across the whole app. Common gaps are no skip-to-content link, navigation not wrapped in a nav landmark, and content not wrapped in a single main landmark - which also means SvelteKit's post-navigation focus reset lands users in a structureless page. Keyboard users then tab through the full header on every client-side navigation.
In the root +layout.svelte, add a skip link as the first focusable element pointing to the main content, wrap site navigation in <nav aria-label>, and wrap page content in a single <main id="main-content">. Because the layout is shared, this fixes structure for every route at once and gives SvelteKit's focus management a meaningful place to land. Verify the skip link is visible on focus and actually moves focus to main.
<!-- +layout.svelte -->
<div class="header">...</div>
<slot /> <!-- +layout.svelte -->
<a class="skip-link" href="#main-content">Skip to content</a>
<header>
<nav aria-label="Primary">...</nav>
</header>
<main id="main-content">
<slot />
</main> SvelteKit-Specific Tips
- Keep SvelteKit's built-in accessibility features working: the route announcer only helps if every route sets a unique, descriptive <title> via <svelte:head>, and the post-navigation focus reset only helps if the layout has a real main landmark to land in.
- Treat Svelte's compile-time a11y warnings as errors to fix, not noise to silence; run svelte-check in CI and reserve svelte-ignore for documented false positives only.
- Use real <a href> for navigation (SvelteKit enhances it into client-side routing automatically) and real <button> for actions, instead of click handlers on divs or spans.
- For form actions with use:enhance, associate server validation errors with their fields via aria-describedby, move focus to the first error, and announce success in an aria-live region - while keeping the form usable without JavaScript.
- SvelteKit only manages focus on route navigation, so handle focus yourself for modals, drawers, and revealed content; prefer the native <dialog> element for free focus trapping and Escape handling.
- Announce loading, result counts, and in-place content updates with aria-live regions, since spinners and swapped content are silent to screen reader users.
- Put the skip link, nav landmark, and single main landmark in the root +layout.svelte so the structure is correct on every route at once.
Recommended Tools
svelte-check
Svelte's official type and diagnostics checker surfaces the compiler's accessibility warnings (missing alt, unassociated labels, click-without-key handlers, invalid ARIA) across the project. Run it in CI so SvelteKit's built-in a11y checks are enforced on every pull request.
axe DevTools
Deque's browser-based scanner runs axe-core against the rendered, hydrated SvelteKit app and reports WCAG-referenced issues that compile-time checks cannot catch, such as runtime contrast, focus order, and live-region behavior after client-side navigation.
SvelteKit Accessibility Documentation
SvelteKit's official accessibility documentation, covering the built-in route announcer, post-navigation focus management, the lang attribute, and the developer responsibilities the framework cannot handle automatically.
SvelteKit Accessibility: Where Failures Come From and How to Fix Them
| Plugin / Tool | Layer | Common Failure | What WCAG 2.1 AA Needs | Where to Fix It |
|---|---|---|---|---|
| Route titles / announcer 2.4.2 | Missing or duplicate per-route titles | Unique, descriptive title per route | <svelte:head> in each +page.svelte | |
| Interactive elements 2.1.1 | Clickable divs/spans, no keyboard support | Native <a>/<button>, keyboard operable | Component markup; respect compiler warnings | |
| Form actions (use:enhance) 3.3.1 / 4.1.3 | Errors not tied to fields; result silent | aria-describedby errors, live-region result | Form components + action return values | |
| Modals / revealed content 2.4.3 | No focus move, trap, or return | Focus into/trap/return; dialog semantics | Native <dialog> or custom focus logic | |
| Root layout 2.4.1 | No skip link or landmarks | Skip link, nav and single main landmark | +layout.svelte (applies to every route) |
Frequently Asked Questions
Does SvelteKit handle accessibility automatically?
SvelteKit ships more accessibility features than most frameworks - a route announcer that reads the new page title after client-side navigation, focus management that resets focus after navigating, and compile-time a11y warnings from the Svelte compiler. But these depend on you: the announcer needs a unique
Why is client-side navigation an accessibility concern in SvelteKit?
After hydration SvelteKit swaps content without a full page reload, so by default a screen reader would not know the page changed and keyboard focus would stay on the old link. SvelteKit solves this with a built-in live-region route announcer and a post-navigation focus reset - but the announcer only works if each route sets a unique, descriptive document title via
What standards should a SvelteKit app target?
The practical working target is WCAG 2.1 AA, which underpins the European Accessibility Act, ADA expectations for web apps in the US, and EN 301 549 for EU public-sector and procurement. WCAG 2.2 adds criteria such as focus appearance, target size, and redundant entry that increasingly appear in audits. Automated tools and svelte-check catch part of this; keyboard and screen reader testing of navigation, forms, and modals catches the rest. This is general information, not legal advice.
Further Reading
- React Tutorial Accessibility Mistakes
- Ai Generated Code Accessibility Audit
- Accessible Forms Guide
- Skip To Content Link Accessibility
- Focus Outline Removed Keyboard Accessibility
- Keyboard Navigation Testing
Other CMS Checklists
- Nextjs Accessibility Checklist
- Nuxt Accessibility Checklist
- Astro Accessibility Checklist
- Gatsby Accessibility Checklist
Get our free accessibility toolkit
We're building a simple accessibility checker for non-developers. Join the waitlist for early access and a free EAA compliance checklist.
No spam. Unsubscribe anytime.