JSPM

@t-2/accessibility

0.1.0
    • ESM via JSPM
    • ES Module Entrypoint
    • Export Map
    • Keywords
    • License
    • Repository URL
    • TypeScript Types
    • README
    • Created
    • Published
    • Downloads 24
    • Score
      100M100P100Q83945F
    • License MIT

    Framework-agnostic accessibility widget: black & white, reduce motion, readable typography, reading aids, presets, persistence and i18n. Zero runtime dependencies.

    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-contrast on 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 the theme option)
    • CSP-friendly — pass a nonce for the injected stylesheet

    Install

    npm install @t-2/accessibility

    Usage

    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. onMounted already 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(): void

    Internationalization (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 English

    2. Switch language at runtime:

    a11y.setLocale('en'); // re-renders the panel in the new language

    3. 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 in px won'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 to null to 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 page

    License

    MIT