A floating accessibility overlay that gives users control over how they experience a website — text size, contrast, fonts, animations, and more. Built with Svelte 5, Tailwind CSS 4, and TypeScript.
Web accessibility is often an afterthought. Users with visual impairments, dyslexia, motor difficulties, or light sensitivity frequently have no way to adapt a website to their needs. Commercial solutions (UserWay, AccessGo) exist but are heavy, expensive, and opaque.
This widget provides 8 WCAG-aligned accessibility features as a lightweight, open, drop-in component.
| Feature | What it does | WCAG |
|---|---|---|
| Text Size | Scale page font size (85%–175%) | 1.4.4 AA |
| Contrast Mode | High contrast, dark mode, or full inversion | 1.4.3 AA / 1.4.6 AAA |
| Dyslexia Font | Swap to OpenDyslexic across the page | 1.4.12 AA |
| Highlight Links | Outline and underline all links | 2.4.7 AA |
| Pause Animations | Stop CSS animations and media playback | 2.3.1 A |
| Big Cursor | Replace cursor with a larger 48px pointer | — |
| Content Spacing | Increase line-height, letter/word spacing | 1.4.12 AA |
| Reading Guide | Mouse-following highlight bar for line tracking | 1.4.8 AAA |
Each feature is a standalone Svelte component registered via a FeatureDefinition object (id, label, icon, component). The panel doesn't know about individual features — it iterates the registry and renders whatever is registered.
Trade-off: Slightly more indirection vs. dead-simple extensibility. Adding a 9th feature is 3 steps: create the component, add the type, register it. No panel code changes.
The widget needs to override styles across the entire page, not just its own component tree. Svelte's scoped styles can't do this. Instead, a central injector.ts manages a single <style> element in <head>, with each feature owning a slice of the CSS via a Map<featureId, cssString>.
Trade-off: Bypasses Svelte's style scoping (which is the point), but keeps things manageable with one style element instead of scattered DOM mutations.
data-a11y-widget self-exclusion pattern?The widget modifies page-wide styles (font size, contrast filters, cursor) — but it must not break itself. The widget root element carries a data-a11y-widget attribute, and all injected CSS uses :not([data-a11y-widget]) selectors to skip the widget. For filter-based features (contrast modes), the widget additionally resets with filter: none !important.
Trade-off: Selectors are more verbose, but this is the most reliable approach. Alternatives like all: revert or iframe isolation were considered but rejected — revert breaks Tailwind classes inside the widget, and iframes create accessibility/focus-trap problems.
$state at module level instead of a Svelte store?Svelte 5 runes ($state, $effect) replace the legacy store API. A single $state object in a .svelte.ts file acts as a reactive singleton — any component that calls getState() gets a reactive reference that triggers re-renders automatically.
Trade-off: Requires the .svelte.ts file extension (rune processing), but eliminates boilerplate (no writable(), no $ prefix, no subscribe).
localStorage to $effect?SvelteKit runs server-side rendering (SSR) by default. localStorage doesn't exist on the server. Instead of disabling SSR globally, the state initializes with defaults and hydrates from localStorage inside an $effect (which only runs client-side).
Trade-off: Brief flash of default state before hydration vs. SSR compatibility. In practice, the flash is invisible because the widget panel starts closed.
| Technology | Version | Why |
|---|---|---|
| SvelteKit | latest | Framework with SSR, routing, file-based structure |
| Svelte 5 | latest | Runes API — simpler reactivity model |
| Tailwind CSS 4 | latest | CSS-first config (@theme), no JS config file |
| Lucide Svelte | latest | Tree-shakable icon library |
| TypeScript | strict | Type safety across state, props, and registry |
src/lib/a11y-widget/
├── AccessibilityWidget.svelte # Floating button + panel toggle
├── AccessibilityPanel.svelte # Settings panel (iterates registry)
├── state.svelte.ts # Reactive state singleton
├── types.ts # TypeScript interfaces
├── injector.ts # Runtime CSS injection engine
├── persistence.ts # localStorage adapter
├── cursor.ts # Big cursor SVG generator
├── features/
│ ├── registry.ts # Feature registration
│ ├── index.ts # Barrel (registers all features)
│ └── [Feature].svelte # One per feature (8 total)
└── assets/
└── OpenDyslexic-Regular.woff2
npm install
npm run dev
Click the accessibility button (bottom-right corner) to open the panel. Toggle features, reload the page to verify persistence, open DevTools to inspect the injected <style> element.