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 the mount hook and clean up on
unmount. A template ref is the cleanest container.
Composition API (<script setup>):
<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>Options API:
<script lang="ts">
import { defineComponent } from 'vue';
import { Accessibility } from '@t-2/accessibility';
export default defineComponent({
data() {
// Not reactive — keep the instance off Vue's reactivity to avoid proxying.
return { a11y: null as Accessibility | null };
},
mounted() {
this.a11y = new Accessibility({
container: this.$refs.container as HTMLElement,
locale: 'sl',
});
},
beforeUnmount() {
this.a11y?.destroy();
},
});
</script>
<template>
<div ref="container"></div>
</template>Nuxt / SSR: construct it on the client only.
mounted/onMountedalready run 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. |
features |
Partial<Features> |
see below | Which controls the panel offers. |
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.
Choosing which controls to show
Use the features option to enable/disable individual controls. Only a curated
set is on by default; pass overrides to turn others on (or off).
new Accessibility({
container: '#a11y-panel',
features: {
lineHeight: true, // turn on a control that's off by default
pauseMedia: false, // turn off a default one
invert: true, // add another colour mode to the colour group
},
});On by default: grayscale (black & white), font, fontScale (text size),
underlineLinks, highlightLinks, stopAnimations, pauseMedia, hideImages.
Off by default: presets, highContrast, invert, lowSaturation,
lineHeight, letterSpacing, textAlign, highlightHeadings, bigCursor,
readingGuide.
Color modes (grayscale, highContrast, invert, lowSaturation) are
individual features — each enabled one appears as an option in the colour group;
if none are enabled the colour section is hidden. The full default set is
exported as DEFAULT_FEATURES, and the Features type lists every key.
Note:
featurescontrols what the panel shows. A disabled control's underlying state still exists (e.g. set viainitialStateor persisted) — it just can't be changed from the UI.
Theming
The panel is styled with --a11yw-* CSS custom properties. Override any of them
via the theme option (set on the widget root) or your own CSS targeting
.a11yw-ui. Every variable with its default is shown below — pass only the ones
you want to change.
new Accessibility({
container: '#a11y-panel',
theme: {
'--a11yw-font': 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif',
'--a11yw-accent': '#1a4fd6', // borders/outlines of active & focused controls
'--a11yw-accent-strong': '#103bb0', // text of the active control
'--a11yw-accent-soft': '#e7eeff', // background of the active control
'--a11yw-bg': '#ffffff', // panel background
'--a11yw-fg': '#1a1a1a', // panel text
'--a11yw-muted': '#6a6a6a', // section titles
'--a11yw-border': '#d2d6dc', // button borders
'--a11yw-btn-bg': '#f7f8fa', // button background
'--a11yw-radius': '14px', // panel corner radius
'--a11yw-btn-radius': '10px', // button corner radius
'--a11yw-danger': '#c0282d', // reset button text
'--a11yw-danger-border': '#d2353a', // reset button border
'--a11yw-danger-soft': '#fdecec', // reset button hover background
},
});The equivalent in plain CSS (override only the ones you want to change):
.a11yw-ui {
--a11yw-font: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
--a11yw-accent: #1a4fd6; /* borders/outlines of active & focused controls */
--a11yw-accent-strong: #103bb0; /* text of the active control */
--a11yw-accent-soft: #e7eeff; /* background of the active control */
--a11yw-bg: #ffffff; /* panel background */
--a11yw-fg: #1a1a1a; /* panel text */
--a11yw-muted: #6a6a6a; /* section titles */
--a11yw-border: #d2d6dc; /* button borders */
--a11yw-btn-bg: #f7f8fa; /* button background */
--a11yw-radius: 14px; /* panel corner radius */
--a11yw-btn-radius: 10px; /* button corner radius */
--a11yw-danger: #c0282d; /* reset button text */
--a11yw-danger-border: #d2353a; /* reset button border */
--a11yw-danger-soft: #fdecec; /* reset button hover background */
}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