AccessScanRun a free scan

Guide

Svelte Accessibility: Compiler Warnings, Focus, and Accessible Components

Svelte is one of the few frameworks that ships accessibility checks in the box. The compiler flags missing alt text, mismatched ARIA, and non-interactive elements with click handlers before you ever open a browser. That head start is real, but it stops at the markup you write at build time. The hard parts of Svelte accessibility, especially focus management across SvelteKit's client-side router, are entirely on you.

This guide covers four areas that matter in production: the a11y warnings the compiler actually emits, semantic markup in .svelte files, focus handling when SvelteKit navigates without a full page load, and patterns for accessible interactive components. The goal is WCAG 2.2 Level A and AA conformance, which is the baseline the European Accessibility Act enforces from 28 June 2025.

Read the compiler a11y warnings (don't suppress them)

Svelte runs an accessibility linter during compilation. When you write an <img> without alt, a <div on:click> with no keyboard handler, or aria-* attributes that don't match the element's role, you get a warning like a11y_no_static_element_interactions or a11y_missing_attribute right in your terminal. These map directly to WCAG criteria: missing alt fails 1.1.1 Non-text Content, and a click-only div fails 2.1.1 Keyboard.

The temptation is to silence them with <!-- svelte-ignore a11y_no_static_element_interactions -->. Resist it unless you've genuinely handled the issue another way. A click handler on a <div> is almost always a sign you should be using a <button> instead, which gives you keyboard activation, focusability, and the correct role for free:

  • Wrong: <div on:click={open}>Menu</div> triggers a warning and is unreachable by keyboard.
  • Right: <button on:click={open}>Menu</button> is focusable, activates on Enter and Space, and announces as a button.
  • If you must keep a custom element interactive, add role, tabindex="0", and an on:keydown handler for Enter and Space, then the warning is legitimately resolved.

Treat the a11y warning list as a checklist, not noise. Note that Svelte 5 renamed these rules to snake_case (a11y_missing_attribute); older Svelte 3/4 projects use the hyphenated a11y-missing-attribute form in ignore comments. Run a build in CI and fail on these, the same way you would on a type error.

Write semantic markup, not styled divs

The compiler catches missing attributes, but it cannot tell you that your page has no <main>, no heading hierarchy, or six nested <div>s where a <nav> and a <ul> belong. Semantic HTML is what screen readers and landmark navigation rely on, and Svelte's template syntax is just HTML, so there is no framework-specific excuse to reach for divs.

  • Use one <h1> per route, then <h2>/<h3> in order. Don't skip levels for visual sizing; control size with CSS.
  • Wrap primary content in <main> and put a skip link before it to satisfy 2.4.1 Bypass Blocks.
  • Use <nav>, <header>, <footer>, and <button>/<a> for their real purposes. A link navigates; a button performs an action.
  • Label form fields with <label for> and surface validation errors with aria-describedby, not just red borders, to meet 3.3.1 Error Identification.

In a SvelteKit layout, the skip link is a one-time addition that pays off on every page. Pair it with a tabindex="-1" target on <main> so the link can move focus there, which leads directly into the harder problem below. For the broader landmark and heading discipline, our accessibility checklist is a useful pass before you ship.

Manage focus on SvelteKit navigation

This is the single biggest gap in most Svelte accessibility work. When SvelteKit navigates client-side, it swaps the DOM but does not reload the page, so the browser's default behavior of moving focus to the top of a new document never fires. A keyboard or screen reader user clicks a link, the visible content changes, but their focus stays on the link that no longer exists or jumps to <body>. They get no announcement that anything happened, which undermines 2.4.3 Focus Order and leaves the experience broken.

SvelteKit gives you afterNavigate to hook into this. The reliable pattern is to move focus to a heading or the main landmark after each navigation, and to announce the new page title via a polite live region (4.1.3 Status Messages). In your root +layout.svelte:

  • Import afterNavigate from $app/navigation and, inside it, call .focus() on your <main> element (which needs tabindex="-1").
  • Skip the focus move on the very first load, where the browser already places focus correctly, so you only adjust on subsequent in-app navigations.
  • Add a visually hidden aria-live="polite" region and update its text with the new page's title so screen readers announce the route change.
  • Reset scroll position too, since focus and scroll are separate concerns and SvelteKit's default scroll handling won't help an assistive-tech user orient.

For modals and menus opened during a session, the rule is the same but tighter: move focus into the dialog on open, trap it while open, and return focus to the trigger on close. Keyboard reachability across all of this is what 2.1.1 hinges on, and our keyboard accessibility guide walks through the focus-trap and return-focus patterns in detail.

Build accessible interactive components

Custom dropdowns, tabs, accordions, and tooltips are where Svelte accessibility either holds up or falls apart. The compiler will nudge you on obvious ARIA mismatches, but it cannot verify that your aria-expanded actually tracks state or that arrow keys move between tab stops per the ARIA Authoring Practices. Wire ARIA attributes to Svelte's reactive state so they never drift:

  • A disclosure button needs aria-expanded={open} and aria-controls pointing at the panel's id. Because it's reactive, the attribute updates the instant state changes.
  • Custom controls still need keyboard support: Escape to close, arrow keys to move within a composite widget, Enter/Space to activate.
  • Respect prefers-reduced-motion for any transition; Svelte's transition: directives run regardless, so gate them on the media query to avoid vestibular harm.
  • Check that interactive targets are at least 24x24 CSS pixels (2.5.8 Target Size Minimum, new in WCAG 2.2) and that focus indicators stay visible (2.4.11 Focus Not Obscured, also new in 2.2).

Color and contrast are component-level concerns too: text needs 4.5:1 (3:1 for large text, at least 18pt or 14pt bold), and UI components and focus rings need 3:1 against adjacent colors. Verify your design tokens with a contrast checker rather than eyeballing them. Automated tooling is fast but partial. Run a free accessibility scan to catch the machine-detectable issues across your routes, then test the focus and keyboard flows by hand, because no scanner can confirm that your SvelteKit navigation actually announces and moves focus correctly.

Check your site against AccessScan

See your issues ranked by impact in seconds — free.

Run a free accessibility scan

FAQ

Does Svelte's compiler make my app accessible automatically?

No. The compiler catches a useful subset of static markup issues, such as missing alt text, ARIA mismatches, and click handlers on non-interactive elements, but it cannot evaluate focus management, keyboard interaction logic, contrast, or screen reader announcements. Those require manual testing against WCAG 2.2 A and AA.

How do I fix focus not moving after SvelteKit client-side navigation?

Use the afterNavigate hook from $app/navigation in your root layout to move focus to your <main> element (given tabindex="-1") on each navigation, skipping the first load. Pair it with an aria-live="polite" region that announces the new page title so screen reader users know the route changed.

Is it safe to use svelte-ignore to silence a11y warnings?

Only when you've genuinely resolved the underlying issue through other means, for example adding role, tabindex, and a keydown handler to a custom interactive element. Suppressing a warning to make it go away usually hides a real WCAG failure, most often a div that should have been a button.

Which WCAG version applies to Svelte apps in the EU?

The European Accessibility Act, in force from 28 June 2025, sets WCAG 2.2 Level A and AA as the baseline via EN 301 549 for covered products and services. New 2.2 criteria like Target Size Minimum and Focus Not Obscured are especially relevant to custom Svelte components.

More guides