A configurable, open-source accessibility panel for Svelte 5 apps. Lets users adjust your site for their needs without any server-side setup.
Originally built for Care Culture's public website, then extracted into a standalone package.
Adds an accessibility widget that lets site visitors adjust your site for their needs:
The panel UI renders in a Shadow DOM — fully isolated from your site's CSS.
Note: The accessibility effects actively reach into your page (injecting CSS, appending overlays, muting audio). That is the point. See Host page effects below.
Full docs and live demo: https://a11y.clmartin.dev
pnpm add svelte-a11y-panel
# or
npm install svelte-a11y-panel
Add PanelMount and AccessibilityButton to your root layout:
<!-- src/routes/+layout.svelte -->
<script>
import { PanelMount, AccessibilityButton } from 'svelte-a11y-panel';
let { children } = $props();
</script>
<PanelMount config={{
accentColor: '#2563eb',
statement: {
orgName: 'Your Organisation',
email: '[email protected]',
assessmentDate: 'January 2026',
}
}} />
<AccessibilityButton accentColor="#2563eb" />
{@render children()}
PanelMount renders nothing visible — it sets up the panel in a Shadow DOM. AccessibilityButton is a fixed-position floating button (bottom-right) that opens and closes the panel.
Prefer a guided setup? Run from the root of your SvelteKit project:
npx svelte-a11y-panel init
The CLI detects your layout file and adds a configured PanelMount with your brand colour, org name, and contact email pre-filled.
Pass a config object to PanelMount:
<PanelMount config={myConfig} />
| Option | Type | Default | Description |
|---|---|---|---|
accentColor |
string |
'#2563eb' |
Colour for buttons, toggles, focus rings, and host-page overlays |
uiFontFamily |
string |
'system-ui, sans-serif' |
Font for the panel UI and all overlays |
dyslexiaFontUrl |
string |
jsDelivr CDN | WOFF2 URL for the OpenDyslexic font |
storageKey |
string |
'a11y-panel-state' |
localStorage key for persisted state |
positionKey |
string |
'a11y-panel-pos' |
sessionStorage key for dragged panel position |
statement.orgName |
string |
'' |
Organisation name in accessibility statement |
statement.email |
string |
'' |
Contact email in accessibility statement |
statement.conformanceStatus |
string |
WCAG 2.1 AA string | Conformance statement text |
statement.limitations |
string[] |
[] |
Known limitations to list |
statement.assessmentDate |
string |
'' |
Date string for the statement |
Config values (accentColor, dyslexiaFontUrl) are interpolated into a CSS stylesheet injected into the Shadow DOM. Do not set these from untrusted user input or unvalidated CMS fields. Treat config as a build-time constant, not a runtime user setting.
The panel renders inside a Shadow DOM — your page's CSS (including custom properties on :root) cannot reach it. Theming is done through config:
accentColoruiFontFamily<PanelMount config={{
accentColor: '#7c3aed',
uiFontFamily: "'Inter', system-ui, sans-serif",
}} />
| Prop | Type | Default | Description |
|---|---|---|---|
accentColor |
string |
'#2563eb' |
Background colour of the button |
label |
string |
'Accessibility options' |
Accessible label for screen readers |
class |
string |
'' |
Additional CSS classes |
AccessibilityButton is intentionally simple. Build your own trigger using the state functions directly:
<script>
import { openPanel, closePanel, getOpen } from 'svelte-a11y-panel';
let buttonEl = $state(null);
</script>
<button
bind:this={buttonEl}
onclick={() => getOpen() ? closePanel() : openPanel(buttonEl)}
aria-expanded={getOpen()}
aria-controls="a11y-panel"
aria-label="Accessibility options"
>
Accessibility settings
</button>
openPanel(element) takes your trigger element so the panel can return focus to it when closed. getOpen() is a reactive getter backed by Svelte 5 $state.
When users enable features, the panel actively modifies your page:
| Feature | What it does to your page |
|---|---|
| Font size / contrast / filters | Injects <style id="a11y-panel-host-styles"> into <head> |
| Reading guide / mask / magnifier | Appends overlay <div>s to <body> |
| Mute sounds | Sets .muted = true on all <audio> and <video> elements |
| Hide emoji | Wraps emoji text nodes in <span data-a11y-panel-emoji> |
| Link navigator | Appends a <dialog> to <body> |
| Virtual keyboard | Appends a keyboard <div> to <body>, dispatches synthetic KeyboardEvents |
| Text-to-speech | Attaches a click listener to document, uses window.speechSynthesis |
| Voice navigation | Uses window.SpeechRecognition, calls window.scrollBy / history |
| Navigation keys | Attaches a keydown listener to document |
| State persistence | Saves to localStorage under config.storageKey |
All effects are fully reversed when the user turns them off or the panel is unmounted.
| Feature | Support |
|---|---|
| Panel UI | All modern browsers |
| Text-to-speech | All modern browsers |
| Voice navigation | Chrome / Edge only (Web Speech Recognition API) |
| Virtual keyboard | All modern browsers |
Pass a customStatement snippet to PanelMount to replace the default statement content entirely:
<PanelMount config={myConfig}>
{#snippet customStatement()}
<h2>Our Accessibility Statement</h2>
<p>We are committed to making our site accessible to everyone.</p>
<p>Contact us at <a href="mailto:[email protected]">[email protected]</a>.</p>
{/snippet}
</PanelMount>
When customStatement is provided, the default statement is replaced entirely by your content. The back button and statement header are still rendered.
If your site uses a strict CSP, you will need to allow the following:
| Feature | CSP directive required |
|---|---|
| Host-page style injection | style-src 'unsafe-inline' (or a nonce) |
| OpenDyslexic font (if using dyslexia mode) | font-src cdn.jsdelivr.net |
| Self-hosted font | font-src 'self' (set dyslexiaFontUrl to your own URL) |
If 'unsafe-inline' is blocked, the panel UI still works — only host-page style overrides (font changes, contrast filters, cursor overrides) will be silent no-ops.
Voice navigation uses the browser's SpeechRecognition API, which requires microphone permission. The browser will prompt the user the first time they enable voice navigation. Speech is processed entirely in the browser — no audio data is sent to any server.
Text-to-speech reads aloud the text content of any element the user clicks. On pages with sensitive information, users should be aware that reading aloud may expose content to bystanders.
localStorage stores the user's accessibility preferences (toggle states, font size, colours). No personally identifiable information is stored.
CDN font: When dyslexia mode is enabled, a request is made to cdn.jsdelivr.net. To avoid this, provide your own font URL via dyslexiaFontUrl.
MIT — see LICENSE