Package Exports
- @t-2/accessibility
Readme
@t-2/accessibility
A framework-agnostic accessibility widget for any website. Zero runtime dependencies, written in TypeScript. Render an inline panel that lets visitors adjust color, motion, typography and reading aids — or drive the engine headlessly from your own UI.
Features
- Color & contrast — black & white (grayscale), high contrast, invert/dark, low saturation
- Links — underline links, highlight links
- Typography — text size, line height, letter spacing, legible font, dyslexia font (OpenDyslexic), text alignment, highlight titles
- Motion — stop / reduce animations, pause audio/video
- Reading aids — reading guide, big cursor
- Page — hide images
- Profiles — one-click presets: Dyslexia, Low vision, Seizure safe, ADHD friendly
- Persistence — remembers choices in
localStorage - Respects OS preferences — seeds from
prefers-reduced-motion/prefers-contraston first visit - i18n — every label is overridable
- Accessible by design — WAI-ARIA radio groups with arrow-key navigation, each group labelled via
aria-labelledby, ARIA control states, honors reduced motion - Themeable — restyle the panel with
--a11yw-*CSS custom properties (or thethemeoption) - CSP-friendly — pass a
noncefor the injected stylesheet
Install
npm install @t-2/accessibilityUsage
The widget renders an always-visible panel of controls into a container element (or a CSS selector). It works in any project — below are the three common setups.
Plain HTML project
With a CDN, no build step — the global A11y is exposed by the IIFE bundle:
<div id="a11y-panel"></div>
<script src="https://cdn.jsdelivr.net/npm/@t-2/accessibility/dist/index.global.js"></script>
<script>
new A11y.Accessibility({ container: '#a11y-panel' });
</script>With a bundler (Vite, webpack, etc.):
<div id="a11y-panel"></div>import { Accessibility } from '@t-2/accessibility';
new Accessibility({ container: '#a11y-panel' });Vue 3 project
It's framework-agnostic, so you just create it in onMounted and clean up in
onBeforeUnmount. A template ref is the cleanest container.
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { Accessibility } from '@t-2/accessibility';
const container = ref<HTMLElement | null>(null);
let a11y: Accessibility | null = null;
onMounted(() => {
a11y = new Accessibility({ container: container.value!, locale: 'sl' });
});
onBeforeUnmount(() => {
a11y?.destroy(); // removes DOM, listeners and injected styles
});
</script>
<template>
<div ref="container"></div>
</template>Nuxt / SSR: construct it on the client only.
onMountedalready runs client-side; or wrap the container in<ClientOnly>. Building it without a DOM is a harmless no-op, but the panel needs a real element to render into.
To drive it from your own Vue UI instead, use mount: false and keep Vue in
sync via the change event:
const a11y = new Accessibility({ mount: false });
const state = ref(a11y.getState());
a11y.on('change', (s) => (state.value = s));
// a11y.set('colorMode', 'grayscale'), a11y.toggle('underlineLinks'), ...Headless (build your own UI, any framework)
const a11y = new Accessibility({ mount: false });
a11y.set('colorMode', 'grayscale');
a11y.toggle('underlineLinks');
a11y.setFontScale(1.25);
a11y.applyPreset('dyslexia');
a11y.on('change', (state) => console.log(state));
a11y.reset();Options
| Option | Type | Default | Description |
|---|---|---|---|
mount |
boolean |
true |
Render the built-in inline panel. |
container |
HTMLElement | string |
document.body |
Where to render the panel (element or selector). |
persist |
boolean |
true |
Save state to localStorage. |
storageKey |
string |
'a11y-widget' |
Storage key. |
respectOSPreferences |
boolean |
true |
Seed first-time state from OS prefs. |
locale |
string |
'en' |
Bundled locale code ('en', 'sl'). Unknown → English. |
labels |
Partial<Labels> |
— | Override individual UI strings (applied over the locale). |
initialState |
Partial<A11yState> |
— | Initial state over defaults/persisted. |
dyslexiaFontUrl |
string | null |
CDN OpenDyslexic | woff2 for the dyslexia @font-face; null disables. |
nonce |
string |
— | CSP nonce applied to the injected <style>. |
theme |
Record<string,string> |
— | CSS custom property overrides set on the widget root. |
API
getState(): A11yState
set(key, value): this
toggle(booleanKey): this
setFontScale(n) / setLineHeight(n) / setLetterSpacing(n): this
applyPreset(name): this // 'dyslexia' | 'vision-impaired' | 'seizure-safe' | 'adhd-friendly'
update(partial): this
reset(): this
on('change', listener): () => void
setLocale(code): this // switch bundled locale, re-renders UI
setLabels(partial): this // override individual strings, re-renders UI
getLabels(): Labels
element: HTMLElement | null // the mounted panel's root, or null
destroy(): voidInternationalization (i18n)
English (en) and Slovenian (sl) ship built in. Resolution order is
English defaults → chosen locale → your overrides, so partial translations
and one-off tweaks both work. Everything can change at runtime.
1. Pick a bundled locale at startup:
new Accessibility({ locale: 'sl' }); // unknown codes fall back to English2. Switch language at runtime:
a11y.setLocale('en'); // re-renders the panel in the new language3. Override individual strings (applied on top of the active locale, and
preserved across setLocale):
new Accessibility({ locale: 'sl', labels: { title: 'Pripomočki' } });
// or later:
a11y.setLabels({ reset: 'Počisti vse' });4. Add a language that isn't bundled — pass a full catalog. Start from the
exported en object so you can't miss a key (the Labels type lists them all):
import { Accessibility, en, type Labels } from '@t-2/accessibility';
const fr: Labels = {
...en,
title: 'Accessibilité',
reset: 'Tout réinitialiser',
colorTitle: 'Couleur et contraste',
// …translate the rest
};
new Accessibility({ labels: fr });The catalogs (en, sl) and the locales registry are exported and
tree-shakeable. In Vue, pass locale/labels the same way, and call
a11y.setLocale(...) when your app's language changes.
Theming
The panel is styled with --a11yw-* CSS custom properties. Override them via the
theme option (set on the widget root) or your own CSS targeting .a11yw-ui.
new Accessibility({
container: '#a11y-panel',
theme: {
'--a11yw-accent': '#e2001a',
'--a11yw-radius': '8px',
},
});Available variables: --a11yw-font, --a11yw-accent, --a11yw-accent-strong,
--a11yw-accent-soft, --a11yw-bg, --a11yw-fg, --a11yw-muted,
--a11yw-border, --a11yw-btn-bg, --a11yw-radius, --a11yw-btn-radius,
--a11yw-danger, --a11yw-danger-border, --a11yw-danger-soft.
Content Security Policy (CSP)
The widget injects one <style> element. Under a strict CSP, pass a nonce that
matches your style-src directive:
new Accessibility({ container: '#a11y-panel', nonce: 'PER_REQUEST_NONCE' });Note: a few inline style attributes are still used for dynamic values (the
font-scale CSS variable, the reading-guide position, and theme overrides).
Those require your style-src to allow inline styles; a nonce alone does not
cover inline attributes.
Notes & limitations
- Text scaling works by scaling the root font size, so it affects text sized
in
rem/em/%. Text hard-coded inpxwon't scale — this is a deliberate trade-off to avoid mutating every element on the page. - Color modes use CSS filters on the whole document (including the widget), which is expected behavior.
- Dyslexia font is loaded from a CDN by default. Self-host by passing
dyslexiaFontUrl(an OpenDyslexic woff2), or set it tonullto rely on system fonts. - This widget improves runtime adjustability; it is not a substitute for building an accessible site (semantic HTML, alt text, contrast, etc.).
Development
npm install
npm run build # ESM + CJS + IIFE + d.ts into dist/
npm test # vitest (jsdom)
npm run typecheck # tsc --noEmit
npm run lint # eslint
npm run format # prettier --write
npm run demo # build, then serve the demo pageLicense
MIT