npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@codestrategie/a11y-widget

v1.0.1

Published

Framework-agnostic accessibility widget as a web component (Lit): text size, contrast, read aloud, reading mask, page structure and more – fully themeable, no tracking.

Readme

@codestrategie/a11y-widget

Framework-agnostic accessibility widget as a web component (<cs-a11y-widget>, built with Lit 3 and TypeScript). It gives visitors aids such as bigger text, contrast modes, read aloud, a reading mask and a page-structure navigation, and can be fully customised in colour, font, size and position. Default branding: codestrategie.de (dark surface #1D1D1B, accent #8AE631).

Features

| Group | Feature | |---|---| | Text | Bigger text (1.25x – 2x), line height (1.3x – 2x), text alignment, readable font | | Display | Contrast (dark / light / high), grayscale, hide images, pause animations | | Orientation | Read aloud (3 speeds), highlight links, reading mask, highlight focus, sitemap, page structure |

Highlights:

  • The launcher is keyboard accessible and uses correct ARIA attributes (aria-expanded, aria-controls, aria-haspopup).
  • Real buttons (role="switch", aria-pressed) instead of div checkboxes; clickable step bars with arrow-key support.
  • Centred positions are truly centred; 8 positions, a separate mobile position, hideOnMobile / hideOnDesktop.
  • The focus trap is released on close and focus returns to the launcher.
  • "Hide widget" remembers the choice (session / 24 h / 1 week) and hides the launcher too; active tools stay active.
  • The page structure is collected when the panel opens, not on page load.
  • Grayscale without a filter on <html> (does not break position: fixed, the widget stays in colour).
  • Bigger text / line height restore inline styles exactly and also handle content inserted later.
  • Settings are persisted (localStorage); prefers-reduced-motion and RTL are respected.
  • Everything lives in a Shadow DOM: page CSS and page tools do not affect the widget.
  • No external requests, no tracking.

Installation

Plain HTML (<script>)

<script src="https://example.com/a11y/cs-a11y-widget.iife.js"></script>
<cs-a11y-widget statement-url="/accessibility"></cs-a11y-widget>

Or mount automatically (no element of your own needed):

<script>
  window.CS_A11Y_CONFIG = { primaryColor: '#2563eb', position: 'bottom-left' };
</script>
<script src="https://example.com/a11y/cs-a11y-widget.iife.js"></script>

Alternatively via script attributes: <script src="…iife.js" data-auto-mount data-primary-color="#2563eb" data-position="bottom-left"></script>.

The IIFE build loads the language files at runtime (i18n/<lang>.json), so the folder dist/i18n/ must be served next to cs-a11y-widget.iife.js (see "Internationalisation").

ES module / npm

npm install @codestrategie/a11y-widget
import '@codestrategie/a11y-widget';           // registers <cs-a11y-widget>
// or programmatically:
import { mount } from '@codestrategie/a11y-widget';
mount({ statementUrl: '/accessibility', languages: ['en', 'de', 'fr'] });

Integration

The widget runs anywhere a <script> or a bundler is available. Two variants:

  • IIFE build (dist/cs-a11y-widget.iife.js) for classic websites and CMSs: one script tag, no bundler. The folder dist/i18n/ must sit next to the file (the language is fetched at start-up).
  • ES module (import '@codestrategie/a11y-widget') for frameworks: the bundler packs the languages as lazy chunks automatically; nothing extra has to be served.

The examples set only a few options; all others are listed under "Configuration".

HTML

  1. Put dist/cs-a11y-widget.iife.js and the folder dist/i18n/ on your server, e.g. under /a11y/:

    /a11y/cs-a11y-widget.iife.js
    /a11y/i18n/en.json
    /a11y/i18n/de.json
    /a11y/i18n/…            (all 14 languages)
  2. Add the script and the element (before </body>):

<script src="/a11y/cs-a11y-widget.iife.js" defer></script>
<cs-a11y-widget
  statement-url="/accessibility"
  skip-link-target="#main"
  languages="en,de,fr"
></cs-a11y-widget>

If the language files are not next to the script, set i18n-base-url="/path/to/i18n/".

WordPress

If another accessibility widget or plugin is already active, deactivate it, otherwise two widgets appear.

Preparation: copy cs-a11y-widget.iife.js and i18n/ into the (child) theme folder:

wp-content/themes/my-child-theme/a11y/cs-a11y-widget.iife.js
wp-content/themes/my-child-theme/a11y/i18n/*.json

a) Via the (child) theme's functions.php

<?php
// Load the script in the footer (strategy => defer requires WordPress >= 6.3).
add_action( 'wp_enqueue_scripts', function () {
	$dir = get_stylesheet_directory() . '/a11y/cs-a11y-widget.iife.js';
	$uri = get_stylesheet_directory_uri() . '/a11y/cs-a11y-widget.iife.js';
	wp_enqueue_script(
		'cs-a11y-widget',
		$uri,
		array(),
		file_exists( $dir ) ? (string) filemtime( $dir ) : null,
		array(
			'in_footer' => true,
			'strategy'  => 'defer',
		)
	);
} );

// Output the element at body level (position: fixed!).
add_action( 'wp_footer', function () {
	// Accessibility statement: the page with the slug "accessibility".
	$statement_page = get_page_by_path( 'accessibility' );
	$statement_url  = $statement_page ? get_permalink( $statement_page ) : '';

	$options = array(
		'primary-color'    => '#8AE631',
		'statement-url'    => $statement_url,
		'skip-link-target' => '#main',
		'languages'        => 'en,de,fr', // leave empty = all 14 languages
	);

	$attrs = '';
	foreach ( $options as $name => $value ) {
		if ( '' !== $value ) {
			$attrs .= sprintf( ' %s="%s"', esc_attr( $name ), esc_attr( $value ) );
		}
	}
	echo '<cs-a11y-widget' . $attrs . '></cs-a11y-widget>' . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput
}, 20 );

The language follows the visitor's browser and falls back to <html lang> (WordPress sets e.g. de-DE → de). With a language plugin (WPML, Polylang) you can also set lang per page to follow the site language explicitly.

b) Alternative: must-use plugin (independent of the theme)

Place the files:

wp-content/mu-plugins/cs-a11y-widget.php
wp-content/mu-plugins/cs-a11y-widget/cs-a11y-widget.iife.js
wp-content/mu-plugins/cs-a11y-widget/i18n/*.json

wp-content/mu-plugins/cs-a11y-widget.php:

<?php
/**
 * Plugin Name: CS A11y Widget
 * Description: Accessibility widget (cs-a11y-widget).
 */

defined( 'ABSPATH' ) || exit;

add_action( 'wp_enqueue_scripts', function () {
	$file = __DIR__ . '/cs-a11y-widget/cs-a11y-widget.iife.js';
	wp_enqueue_script(
		'cs-a11y-widget',
		plugins_url( 'cs-a11y-widget/cs-a11y-widget.iife.js', __FILE__ ),
		array(),
		file_exists( $file ) ? (string) filemtime( $file ) : null,
		array( 'in_footer' => true, 'strategy' => 'defer' )
	);
} );

add_action( 'wp_footer', function () {
	$page = get_page_by_path( 'accessibility' );
	printf(
		'<cs-a11y-widget statement-url="%s" skip-link-target="#main"></cs-a11y-widget>' . "\n",
		esc_attr( $page ? get_permalink( $page ) : '' )
	);
}, 20 );

Must-use plugins are loaded automatically (only the PHP file directly in mu-plugins/; subfolders just hold the assets).

React

React 19 supports custom elements natively: props are set as properties if the element knows them, otherwise as attributes. The component only has to appear once in the tree (e.g. in App).

// CsA11yWidget.tsx
import { useEffect, useRef } from 'react';
import '@codestrategie/a11y-widget'; // registers <cs-a11y-widget>
import type { CsA11yWidget as Widget, ActiveTools } from '@codestrategie/a11y-widget';

// JSX types (React 19: React.JSX)
declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'cs-a11y-widget': React.DetailedHTMLProps<React.HTMLAttributes<Widget>, Widget> & {
        'primary-color'?: string;
        'secondary-color'?: string;
        'text-color'?: string;
        position?: string;
        'statement-url'?: string;
        'skip-link-target'?: string;
        languages?: string;
        'language-selector'?: string;
        'i18n-base-url'?: string;
      };
    }
  }
}

export function CsA11yWidget({ onChange }: { onChange?: (tools: ActiveTools) => void }) {
  const ref = useRef<Widget>(null);

  useEffect(() => {
    const el = ref.current;
    if (!el || !onChange) return;
    const handler = (e: Event) => onChange((e as CustomEvent<{ tools: ActiveTools }>).detail.tools);
    el.addEventListener('cs-a11y-change', handler);
    return () => el.removeEventListener('cs-a11y-change', handler);
  }, [onChange]);

  return <cs-a11y-widget ref={ref} statement-url="/accessibility" languages="en,de,fr" />;
}

Complex values (tools, languages as an array, translations) can be set as properties via ref, e.g. useEffect(() => { ref.current!.translations = { en: { 'widget.title': 'Accessibility tools' } }; }, []).

Next.js / SSR: the widget needs window / customElements and must only run in the browser. Load a client component and import the package inside an effect:

'use client';
import { useEffect } from 'react';

export default function A11yWidget() {
  useEffect(() => {
    void import('@codestrategie/a11y-widget'); // registers <cs-a11y-widget> in the browser only
  }, []);
  return <cs-a11y-widget statement-url="/accessibility" />;
}

Include the component e.g. in app/layout.tsx at the end of <body>. No hydration error occurs because the element is rendered empty on the server (it only renders after registration).

Vue 3

// vite.config.ts: vue({ template: { compilerOptions: { isCustomElement: (t) => t === 'cs-a11y-widget' } } })
import '@codestrategie/a11y-widget';
<cs-a11y-widget primary-color="#2563eb" :tools="['contrast', 'bigger-text']" />

Angular

main.ts:

import { bootstrapApplication } from '@angular/platform-browser';
import '@codestrategie/a11y-widget'; // registers <cs-a11y-widget>
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent);

app.component.ts:

import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core';
import type { ActiveTools } from '@codestrategie/a11y-widget';

@Component({
  selector: 'app-root',
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <router-outlet />
    <cs-a11y-widget
      [attr.primary-color]="primaryColor"
      [attr.statement-url]="'/accessibility'"
      [attr.languages]="'en,de,fr'"
      [tools]="tools"
      (cs-a11y-change)="onChange($event)"
    ></cs-a11y-widget>
  `,
})
export class AppComponent {
  primaryColor = '#8AE631';
  tools = ['bigger-text', 'contrast', 'screen-reader']; // property binding for arrays
  onChange(e: Event) {
    console.log((e as CustomEvent<{ tools: ActiveTools }>).detail.tools);
  }
}

With Angular Universal / SSR, keep the import in main.ts (browser entry), not in main.server.ts.

Svelte

<script>
  import '@codestrategie/a11y-widget';
</script>

<cs-a11y-widget primary-color="#2563eb" statement-url="/accessibility"></cs-a11y-widget>

Astro

A <script> without attributes is bundled by Astro and runs in the browser only. In the layout file (src/layouts/Layout.astro):

---
// …
---
<html lang="en">
  <body>
    <slot />
    <cs-a11y-widget statement-url="/accessibility" skip-link-target="#main"></cs-a11y-widget>
    <script>
      import '@codestrategie/a11y-widget';
    </script>
  </body>
</html>

Alternative without bundling: copy the IIFE and i18n/ to public/a11y/ and include it with is:inline:

<cs-a11y-widget statement-url="/accessibility"></cs-a11y-widget>
<script is:inline src="/a11y/cs-a11y-widget.iife.js" defer></script>

Note: the widget uses position: fixed. Ancestors with transform, filter or perspective change the containing block. Render the element at body level or use mount().

Configuration

Every value can be set as an attribute (kebab-case), as a JS property (camelCase), and via mount({…}) / window.CS_A11Y_CONFIG.

| Attribute | Property | Default | Description | |---|---|---|---| | primary-color | primaryColor | #8AE631 | Accent: launcher, header, active states | | secondary-color | secondaryColor | #1D1D1B | Panel background | | text-color | textColor | #FFFFFF | Text colour on the panel | | primary-text-color | primaryTextColor | '' (automatic) | Text/icons on the primary colour; empty = automatic: secondary or text colour if contrast is ≥ 4.5:1 (e.g. #1D1D1B on green), otherwise black/white | | font-family | fontFamily | "Open Sans", system-ui, … | UI font | | heading-font-family | headingFontFamily | "Funnel Display", "Open Sans", … | Heading font | | size | size | medium | small 36 px, medium 44 px, large 64 px | | radius | radius | null (round) | Corner radius of the launcher in px | | icon | icon | person | person, eye, controls, text; custom icon: <svg slot="icon"> | | position | position | bottom-right | top/center/bottom × left/center/right (without center-center) | | mobile-position | mobilePosition | '' (= position) | Position at viewport ≤ 530 px | | offset-x / offset-y | offsetX / offsetY | 24px | Distance from the edge (any CSS length) | | hide-on-mobile | hideOnMobile | false | Hide the launcher at ≤ 530 px | | hide-on-desktop | hideOnDesktop | false | Hide the launcher at > 530 px | | z-index | zIndex | 2147483000 | Base z-index | | lang | lang | '' | Fixed UI language (en de fr es it nl pt pl cs da sv tr uk ar); empty = automatic detection, see "Internationalisation" | | language-selector | languageSelector | true | Language button (globe + code) in the header; turn off with language-selector="false" (special case: boolean defaulting to true; an absent attribute means on) | | languages | languages | [] (all) | Comma-separated list of allowed languages, e.g. languages="en,de,fr"; unknown codes are ignored | | i18n-base-url | i18nBaseUrl | '' | Folder with <lang>.json (own hosting / own texts) | | statement-url | statementUrl | '' | Link to the accessibility statement (footer, only if set) | | sitemap-url | sitemapUrl | '' | Enables the sitemap tool | | tools | tools | [] (all) | Comma-separated list, e.g. tools="bigger-text,contrast"; unknown IDs are ignored | | readable-font-family | readableFontFamily | Verdana, Tahoma, "Atkinson Hyperlegible", … | Font used by "Readable font" | | skip-link-target | skipLinkTarget | '' | CSS selector, e.g. #main – adds a "Skip to content" link | | storage-key | storageKey | cs-a11y | Prefix for storage keys | | no-persist | noPersist | false | Do not persist active tools | | – | translations | {} | Override texts: { en: { 'widget.title': '…' } } (per language, all 14 possible) |

Tool IDs: bigger-text, bigger-line-height, text-align, readable-font, contrast, grayscale, hide-images, pause-animations, screen-reader, highlight-links, reading-mask, focus-outline, sitemap, page-structure.

Internationalisation (i18n)

The widget speaks 14 languages: English (default), German, French, Spanish, Italian, Dutch, Portuguese, Polish, Czech, Danish, Swedish, Turkish, Ukrainian and Arabic (right-to-left). All texts are JSON files in src/i18n/<lang>.json.

  • Lazy per language: only the language actually needed is loaded (plus English as a fallback, see below); until it is available the widget renders nothing. ES build: each language is its own chunk (dist/i18n/*.js). IIFE build: loaded via fetch from i18n/<lang>.json next to the script, so serve dist/i18n/ as well.

  • Language detection, in this order (the first match that is supported and allowed wins):

    1. the language the visitor picked in the widget (persisted; only if the language selector is on and the language is allowed),
    2. the lang attribute on <cs-a11y-widget>,
    3. the browser's preferred languages (navigator.languages, in order; region tags such as pt-BR map to pt); re-evaluated on the languagechange event,
    4. <html lang> of the page,
    5. English.

    languages="en,de,fr" restricts which languages are allowed at every step.

  • Language switcher: by default the header shows a button with a globe and the language code (e.g. "EN"). It opens a searchable list (by name and code); languages are shown in their own language. Arrow keys move the selection, Enter / Space / click apply it; the choice is saved. With language-selector="false" or exactly one language in languages, the button is omitted.

  • Right-to-left: for Arabic the panel gets dir="rtl" (independent of the page); the panel's lang matches the UI language.

  • Per-key fallback: if a text is missing in a language file (e.g. a custom file or a new, not yet translated key), the English text is shown, and as a last resort the key itself – never undefined. If a language fails to load entirely, English is used.

  • Custom translations:

    • Host the files yourself: copy dist/i18n/, adjust the texts and point to them with i18n-base-url="/path/to/i18n/" (<base>/<lang>.json is expected, at least en.json as fallback). Language codes remain the 14 supported ones.

    • Override single texts with the translations property (takes precedence over the files):

      document.querySelector('cs-a11y-widget').translations = {
        en: { 'widget.title': 'Accessibility' },
        fr: { 'widget.title': 'Accessibilité' },
      };

CSS custom properties

All colours are derived from the configuration and can be overridden from outside:

cs-a11y-widget {
  --cs-primary: #2563eb;
  --cs-surface: #ffffff;
  --cs-text: #1d1d1b;
}

| Variable | Meaning | |---|---| | --cs-primary / --cs-on-primary | Accent / text on it (automatically a brand colour with ≥ 4.5:1 contrast, otherwise black/white) | | --cs-surface | Panel background (secondaryColor) | | --cs-text | Text on the panel | | --cs-surface-2, --cs-surface-3, --cs-border, --cs-muted, --cs-primary-tint | derived via color-mix | | --cs-accent-on-surface | --cs-primary, or --cs-text if contrast to the surface is < 3:1 (focus, bars) | | --cs-font, --cs-heading-font | Fonts |

Note: if you override --cs-primary from outside, also set a matching --cs-on-primary.

Methods and events

const w = document.querySelector('cs-a11y-widget');
w.open(); w.close(); w.toggle();
w.reset();                              // turn all tools off
w.setTool('contrast', 'high');          // true | step ID | null
w.getActiveTools();                     // { contrast: 'high', … }
w.hide('week');                         // 'session' | 'day' | 'week'
w.show();

| Event | detail | |---|---| | cs-a11y-open, cs-a11y-close | – | | cs-a11y-change | { tools: Record<ToolId, true \| string> } | | cs-a11y-hide | { duration } |

All events bubble and are composed.

Theming examples

<!-- Light panel with a blue accent -->
<cs-a11y-widget primary-color="#2563eb" secondary-color="#ffffff" text-color="#1d1d1b"></cs-a11y-widget>

<!-- Square text launcher bottom left, bottom centre on mobile -->
<cs-a11y-widget icon="text" size="large" radius="12" position="bottom-left" mobile-position="bottom-center"></cs-a11y-widget>

<!-- Custom icon -->
<cs-a11y-widget>
  <svg slot="icon" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="8"/></svg>
</cs-a11y-widget>

Comparison with other accessibility widgets

Overview

Only facts that could be verified on official sources are listed (checked on 2026-10-13; prices and plans change – see the linked sources).

| | Delivery | Licence | Account needed | Public pricing | |---|---|---|---|---| | cs-a11y-widget | Self-hosted web component, any website or framework, no external requests | MIT | No | Free | | Elementor Ally | WordPress plugin; widget script loaded from the Elementor CDN | Plugin: GPLv2+ | Yes (Elementor connection / API key) | Free plugin, paid plans for additional features | | UserWay | Hosted script (vendor CDN), WordPress plugin available | Commercial | Yes (subscription) | From 490 per year (annual plans) | | accessiBe | Hosted script (vendor CDN) | Commercial | Yes (subscription) | From USD 59 per month or USD 490 per year | | AudioEye | Hosted script | Commercial | Yes | On request (free site scan) | | EqualWeb, Eye-Able, Recite Me | Hosted scripts | Commercial | Yes | Not publicly listed / on request | | Sienna | Self-contained JS bundle (copy & paste) | MIT | No | Free |

What sets cs-a11y-widget apart from these: self-hosted without an account or third-party requests (GDPR-friendly by design), full theming via primary, secondary and text colour, fonts and CSS custom properties, a framework-agnostic web component, and 14 lazy-loaded languages with browser detection. Unlike the commercial services it includes no scanner, monitoring or automated remediation.

Detailed comparison with Elementor Ally

The feature set is comparable to the visitor-facing widget of the WordPress plugin "Ally – Web Accessibility" by Elementor. Differences (as of Ally widget 4.1.4, October 2026):

| | cs-a11y-widget | Elementor Ally (widget) | |---|---|---| | Platform | Any website and framework (web component) | WordPress plugin, widget script served from the Elementor CDN | | Account / API key | Not required | Elementor connection and API key required | | External requests, tracking | None (everything self-hosted) | Script and translations from the CDN, optional analytics | | Colours | Primary, secondary and text colour, fonts, CSS variables | One accent colour | | Tools | 14, incl. read aloud and a page structure with headings, landmarks and links | 14 (read aloud only in paid plans), page structure with headings only | | Languages | 14, loaded individually, custom language files possible | Around 37, from the CDN | | Keyboard | Launcher reachable via Tab, focus returns after closing | Launcher has tabindex="-1" and is not reachable via Tab | | Grayscale | Page grey, widget stays in colour, position: fixed stays intact | Filter on <html>, widget is tinted as well | | Read aloud | Tab key stays usable, texts with separators | Intercepts the Tab key | | Hiding | Default choice is saved, launcher disappears too | Default choice "this session only" is not saved | | Accessibility scanner, AI fixes, statement generator | Not included | Included |

All product names are trademarks of their respective owners. This project is not affiliated with any of the vendors listed above and contains no code from their products.

Read aloud: browser support

"Read aloud" uses the browser's built-in speech synthesis (Web Speech API); no audio is sent to a server by the widget. Voices come from the operating system or the browser, so quality and available languages differ per device.

| Browser | Support | Notes | |---|---|---| | Chrome, Edge, Opera (desktop) | Yes | Uses OS voices plus, in Chrome, Google online voices (these may send text to Google). | | Firefox (desktop) | Yes | On Linux only with speech-dispatcher installed. | | Safari (macOS, iOS, iPadOS) | Yes | Speech must be started by a tap/click; on iPhone it may be muted while the ring/silent switch is on. | | Chrome, Samsung Internet, Firefox (Android) | Yes | Voices from the Android text-to-speech engine. | | In-app browsers on Android (Instagram, Facebook, LinkedIn … – Android WebView) | No | The tool is hidden automatically. | | Chromium/Firefox on Linux without voices | Partly | The API exists but has no voices; the widget announces that read aloud is unavailable. |

Behaviour that applies everywhere:

  • Browsers only allow speech after a user interaction. When the setting is restored on page load, the widget stays quiet until the visitor clicks or focuses something.
  • The tool reads what is clicked or focused and supports Alt+↑ / Alt+↓ to move through the page; Esc stops speaking. On touch devices, tapping a link reads it and follows it at the same time.
  • It complements, but does not replace, real screen readers (NVDA, JAWS, VoiceOver, TalkBack).

Accessibility and limitations

  • The widget itself targets WCAG 2.2 AA: contrast, visible focus indicators, target sizes ≥ 44 px, keyboard operation, reduced motion, RTL.
  • Overlay widgets do not make a website accessible. They add personalisation options but replace neither semantically correct HTML, sufficient contrast, nor testing with assistive technologies. An accessibility statement is still needed. Using this widget does not by itself make a site compliant with any law or standard. Do not market it as a compliance solution: in 2025 the US FTC ordered an overlay vendor to pay USD 1 million over claims that its AI widget could make any website WCAG-compliant.
  • Text enlargement, reading mask etc. work with inline styles and overlays and can collide with heavily script-driven layouts.
  • "Read aloud" uses the browser's Web Speech API – see Read aloud: browser support.
  • GIFs are not affected by "Pause animations"; CSS background images stay visible with "Hide images".
  • Sites with a strict CSP: the widget injects <style> elements; adjust style-src or use a nonce concept if necessary. The IIFE build needs connect-src for the language folder.

Development

npm install
npm run dev         # demo with configurator: http://localhost:5173/demo/
npm run typecheck
npm run build       # dist/cs-a11y-widget.js (ES), dist/cs-a11y-widget.iife.js + dist/i18n/*.json, dist/types

Versioning

Semantic Versioning; commits follow Conventional Commits (feat:, fix:, docs:, chore: …). Breaking changes are marked with ! or BREAKING CHANGE:. See CHANGELOG.md.

Releasing

Releases are prepared by the CI pipeline and published after a manual 2FA approval (npm staged publishing):

  1. Bump the version: npm version <patch|minor|major> --no-git-tag-version and add an entry to CHANGELOG.md.
  2. Commit: git commit -am "chore(release): X.Y.Z".
  3. Tag and push: git tag -a vX.Y.Z -m vX.Y.Z && git push --follow-tags – or create a release with tag vX.Y.Z in the GitLab UI.
  4. The pipeline builds the package, checks that the tag matches package.json and stages it on npm.
  5. A maintainer approves the staged version with 2FA on npmjs.com (Staged Packages) or with npm stage approve <stage-id> (list with npm stage list @codestrategie/a11y-widget). Only then is it installable.

Pre-release versions (1.1.0-beta.1) are staged under the next dist-tag, stable versions under latest. The pipeline needs a masked, protected CI/CD variable NPM_TOKEN: an npm granular access token with "Read and write (stage only)" for the @codestrategie scope and "Bypass 2FA" disabled. Protect the tag pattern v* so only maintainers can start a release.

License

MIT © code strategie