Installation

Install the package from npm:

pnpm add svelte-a11y-panel
# or
npm install svelte-a11y-panel
# or
yarn add svelte-a11y-panel

Requirements: Svelte 5 (runes), SvelteKit 2+, Node 18+.

Quick start

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: 'My Organisation',
    email: 'accessibility@mysite.com',
    assessmentDate: 'January 2026',
  }
}} />

<AccessibilityButton accentColor="#2563eb" />

{@render children()}

PanelMount renders nothing visible — it sets up the panel in a Shadow DOM appended to document.body. AccessibilityButton is a fixed-position floating button (bottom-right) that opens and closes the panel.

No CSS imports needed. The panel manages all its own styles.

AccessibilityButton props

PropTypeDefaultDescription
accentColorstring'#2563eb'Background colour of the button. Should match PanelMount's accentColor.
labelstring'Accessibility options'Accessible label (aria-label) for screen readers.
classstring''Additional CSS class(es) to apply to the button element.

Configuration

Pass a config object to PanelMount:

<PanelMount config={myConfig} />

Full config reference

OptionTypeDefaultDescription
accentColorstring'#2563eb'Hex colour for buttons, toggles, focus rings, and host-page overlays
uiFontFamilystring'system-ui, sans-serif'Font for the panel UI and all overlays (link navigator, virtual keyboard)
dyslexiaFontUrlstringjsDelivr CDNWOFF2 URL for OpenDyslexic font. Override to self-host.
storageKeystring'a11y-panel-state'localStorage key for persisted state
positionKeystring'a11y-panel-pos'sessionStorage key for panel position
statement.orgNamestring''Organisation name in the accessibility statement
statement.emailstring''Contact email in the accessibility statement
statement.conformanceStatusstringWCAG 2.1 AA stringConformance statement text
statement.limitationsstring[][]Known accessibility limitations to list
statement.assessmentDatestring''Date the statement was prepared

Security note

accentColor and dyslexiaFontUrl are interpolated into a CSS stylesheet injected into the Shadow DOM. Do not set these from untrusted user input. Treat config as a build-time constant.

Init CLI

If you'd prefer a guided setup, run the init command from the root of your SvelteKit project:

npx svelte-a11y-panel init

The CLI will:

  • Detect your SvelteKit project
  • Ask for your accent colour, org name, contact email, and statement date
  • Modify (or create) src/routes/+layout.svelte with a configured PanelMount
  • Show you the next step (adding AccessibilityButton)

The CLI only runs when you invoke it — it is not a postinstall hook and never runs automatically.

Theming

The panel renders inside a Shadow DOM, so your page's global CSS cannot reach it. Theming is done entirely through config:

  • Accent colour (buttons, toggles, focus rings, overlays) — set accentColor in config.
  • Font (panel UI and all overlays) — set uiFontFamily in config.
<PanelMount config={{
  accentColor: '#7c3aed',
  uiFontFamily: "'Inter', system-ui, sans-serif",
}} />

Colours and fonts set via CSS on the host page (including custom properties on :root) cannot reach inside the Shadow DOM. Use config instead.

Custom accessibility statement

To fully replace the default statement content, pass a customStatement snippet to PanelMount:

<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:access@example.com">access@example.com</a>.
    </p>
  {/snippet}
</PanelMount>

When customStatement is provided, the default statement is replaced entirely. The back button and header are still rendered. For simple cases (org name, email, date), use the statement config fields instead.

Custom trigger button

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 — it re-runs any template that reads it.

CSP & privacy

Content Security Policy

If your site uses a strict CSP, you will need to permit the following:

FeatureCSP directive required
Host-page style injectionstyle-src 'unsafe-inline' (or a nonce)
OpenDyslexic font (dyslexia mode)font-src cdn.jsdelivr.net
Self-hosted fontfont-src 'self' — set dyslexiaFontUrl to your own URL

If style-src 'unsafe-inline' is blocked, the panel UI still works but host-page effects (font overrides, contrast filters, cursor changes) will be silent no-ops.

Microphone access

Voice navigation uses window.SpeechRecognition, which requests microphone permission from the user the first time it is enabled. Speech is processed entirely in the browser — no audio is sent to any server. You may want to mention microphone use in your privacy policy.

localStorage

User accessibility preferences are persisted to localStorage. 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.

Host page effects

When users enable accessibility features, the library actively manipulates the host page. This is intentional — it is the only way to apply adjustments across the entire site.

EffectWhat it does
Style injectionInjects <style id="a11y-panel-host-styles"> into <head>
Reading guide / maskAppends overlay <div>s to <body>
Text magnifierAppends a magnifier <div> that follows the cursor
Link navigatorAppends a <dialog> listing all a[href] elements
Virtual keyboardAppends a keyboard <div>; dispatches synthetic KeyboardEvents
Text-to-speechAttaches a click listener to document; uses window.speechSynthesis
Voice navigationUses window.SpeechRecognition; calls window.scrollBy, history.back()
Navigation keysAttaches a keydown listener to document
Mute soundsSets muted = true on all <audio> and <video> elements
Hide emojiWraps emoji text nodes in <span>s with a hidden class

All effects are fully reversed when the user turns them off or the panel is unmounted.

Browser support

FeatureSupport
Panel UIAll modern browsers
Text-to-speechAll modern browsers
Voice navigationChrome / Edge only (Web Speech Recognition API)
Virtual keyboardAll modern browsers