Documentation · itzsa
Accessibility Toolbar — @itzsa/a11y-toolbar
Floating preference panel for text size, contrast, spacing, motion, and reading aids — including Read Aloud with Nepali (नेपाली) / Hindi Devanagari voice selection. Preferences persist in localStorage and apply via CSS under [data-a11y-content]. The floating control on this site is the live demo (full-height panel by default).
Scope
This toolbar is WCAG-grounded chrome — dialog patterns, focus, live regions, spacing presets. It does not make an inaccessible host page WCAG-compliant. Keep semantic HTML, keyboard support, and contrast in your base UI.Installation#
Add the package and import styles. Load Outfit / Poppins / Noto Sans Devanagari in the host for locale fonts.
pnpm add @itzsa/a11y-toolbarPeers
Peer deps:react and react-dom ^18 or ^19.Prefer a script tag instead of npm? Copy a CDN URL (same assets as WordPress / CDN):
CSS
https://itzsa.acharya-suman.com.np/cdn/a11y-toolbar/a11y-toolbar.min.css
JS
https://itzsa.acharya-suman.com.np/cdn/a11y-toolbar/a11y-toolbar.min.js
How it works#
FOUC bootstrap, content-root scoping, and chrome design — preference updates flow to HTML attrs and CSS tokens.
Architecture flowchart
How preferences reach the page
Before paint
FOUC bootstrap in <head>
getA11yFoucScript() reads localStorage and sets data-a11y-* on <html> so the first paint already matches saved prefs.Two trees in the host app
Host markup
data-a11y-content
CSS
styles.css effect rules
html[data-a11y-text-size] [data-a11y-content] apply tokens — never restyle the toolbar chrome accidentally.Sibling mount
A11yToolbar (outside content)
data-a11y-content so host zoom / filters do not distort the controls.Design
Launcher → panel → cards
A11Y_FEATURE_REGISTRY; each card toggles or cycles a preference.Runtime loop
Preference update path
- User action (card, shortcut, or host API) updates
A11yPreferences - Persist to
localStorage+ announce via live region applyA11yPreferencesmirrors attrs + CSS vars onto<html>- Package CSS re-renders the content root immediately
Input
Shortcuts
DEFAULT_A11Y_SHORTCUTS — panel + features; remap with mergeA11yShortcutsTheme
theme / style / CSS
--itzsa-a11y-* tokens. Host CSS can override the same vars on :root.Scope
Not a WCAG seal
Getting started#
Three pieces: FOUC script in head, SSR content wrapper, Client Component mount outside that wrapper.
import { A11yToolbar, NE_MESSAGES } from "@itzsa/a11y-toolbar";
import { getA11yFoucScript } from "@itzsa/a11y-toolbar/headless";
import "@itzsa/a11y-toolbar/styles.css";
// 1. FOUC bootstrap in root layout <head> (Server Component — /headless)
const a11yFouc = getA11yFoucScript();
// render: script with dangerouslySetInnerHTML={{ __html: a11yFouc }}
// 2. SSR content root — required for FOUC + scoped effects
<main data-a11y-content>{children}</main>
// 3. Mount once from a Client Component, outside the content root
<A11yToolbar
position="bottom-center"
panelAlign="left"
defaultLocale="en"
locales={{ ne: NE_MESSAGES }}
// omit panelMaxHeight for full-height stretch; or pass e.g. "70dvh" to cap
theme={{
accent: "var(--accent)",
header: "#15805f",
headerForeground: "#ffffff",
launcher: "var(--accent)",
launcherForeground: "#ffffff",
launcherRing: "#ffffff",
}}
/>Try it
Use the floating accessibility button, or press Alt+A. Open Language in the header to switch English / नेपाली. For Nepali Read Aloud, pick नेपाली, enable Read Aloud, then click a paragraph under[data-a11y-content]— requires a Nepali (or Hindi) TTS voice installed on the device.React (Next.js)#
FOUC in the root layout Server Component; mount the toolbar from a Client Component outside data-a11y-content.
// app/layout.tsx — Server Component
import { getA11yFoucScript } from "@itzsa/a11y-toolbar/headless";
import { A11yToolbarClient } from "./a11y-toolbar-client";
export default function RootLayout({ children }: { children: React.ReactNode }) {
const a11yFouc = getA11yFoucScript();
return (
<html lang="en">
<head>
<script dangerouslySetInnerHTML={{ __html: a11yFouc }} />
</head>
<body>
<main data-a11y-content>{children}</main>
<A11yToolbarClient />
</body>
</html>
);
}
// a11y-toolbar-client.tsx
"use client";
import { A11yToolbar, NE_MESSAGES } from "@itzsa/a11y-toolbar";
import "@itzsa/a11y-toolbar/styles.css";
export function A11yToolbarClient() {
return (
<A11yToolbar
position="bottom-center"
panelAlign="left"
locales={{ ne: NE_MESSAGES }}
/>
);
}Vite + React#
Inject the FOUC bootstrap before createRoot, wrap the app in data-a11y-content, mount A11yToolbar as a sibling.
// main.tsx — Vite + React
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { A11yToolbar, NE_MESSAGES } from "@itzsa/a11y-toolbar";
import { getA11yFoucScript } from "@itzsa/a11y-toolbar/headless";
import "@itzsa/a11y-toolbar/styles.css";
import App from "./App";
// FOUC bootstrap before first paint
const boot = document.createElement("script");
boot.textContent = getA11yFoucScript();
document.head.appendChild(boot);
createRoot(document.getElementById("root")!).render(
<StrictMode>
<div data-a11y-content>
<App />
</div>
<A11yToolbar
position="bottom-right"
locales={{ ne: NE_MESSAGES }}
/>
</StrictMode>,
);Keyboard shortcuts#
Scalable registry: defaults cover panel + common features. Remap with mergeA11yShortcuts, or pass shortcuts={false} for panel-only.
import {
A11yToolbar,
DEFAULT_A11Y_SHORTCUTS,
mergeA11yShortcuts,
} from "@itzsa/a11y-toolbar";
// Defaults: Alt+A panel, Alt+Shift+R reset, Alt+Shift± text size, …
<A11yToolbar />
// Remap panel toggle only (feature shortcuts stay)
<A11yToolbar hotkey={{ altKey: true, shiftKey: true, key: "a" }} />
// Panel hotkey only — no feature shortcuts
<A11yToolbar shortcuts={false} />
// Scalable custom map
<A11yToolbar
shortcuts={mergeA11yShortcuts(DEFAULT_A11Y_SHORTCUTS, [
{ id: "reset", keys: null }, // remove
{
id: "textSizeInc",
keys: { altKey: true, key: "]" },
action: { type: "feature", feature: "textSize", mode: "inc" },
label: "Increase text size",
},
])}
/>Defaults
Alt+A panel · Alt+Shift+R reset · Alt+Shift+=/− text size · Alt+Shift+C/M/G/L/D contrast / pause / guide / links / dyslexia aid. Ignored while focus is in inputs. Feature shortcuts respectfeatures flags.Features#
Defined in A11Y_FEATURE_REGISTRY. Hide any control with the features prop.
<A11yToolbar
features={{
colorFilter: false,
biggerCursor: false,
readingGuide: false,
}}
/>Display#
Text, contrast, filters, and link/image aids.
Display controls
| Prop | Type | Default | Description |
|---|---|---|---|
| textSize | stepped (0–3) | - | Content zoom (112.5% / 125% / 145%). Uses CSS zoom when supported. |
| textSpacing | stepped (0–2) | - | Letter + word spacing. Max meets WCAG 1.4.12 floors. |
| lineHeight | stepped (0–2) | - | Line height 1.5 / 1.75 / 2 (all ≥ 1.5). |
| fontSelection | stepped (0–2) | - | Default / system UI / readable serif on content. |
| textAlign | stepped (0–2) | - | Left / center / right. |
| dyslexiaFriendly | toggle | - | Spacing-only reading aid (max spacing + line height). No bundled font in v1. |
| highContrast | stepped (0–2) | - | Stronger text/UI contrast on content. |
| colorFilter | stepped (0–3) | - | Grayscale / hue / sepia presentation aids — not clinical CVD tools. |
| saturation | stepped (0–2) | - | Full / reduced / none color saturation. |
| hideImages | toggle | - | Hides photos/media via visibility; keeps labeled icons in controls. |
| highlightLinks | toggle | - | Emphasize anchors for low-vision scanning. |
Motion & assist#
Motion reduction and pointer reading aids.
Motion & assist
| Prop | Type | Default | Description |
|---|---|---|---|
| pauseAnimations | toggle | - | Stops animation, transition, and scroll-behavior on content. Additive with prefers-reduced-motion. |
| biggerCursor | toggle | - | 32×32 SVG cursor (keyword fallback auto). Applies page-wide immediately — including over the open panel/overlay. |
| readingGuide | toggle | - | Horizontal reading band that follows the pointer. |
| readAloud | toggle | - | Click-to-speak on p/h1–h6/li/blockquote under data-a11y-content. Locale ne → ne-NP + Nepali/Hindi voice when installed. Panel exposes pause/resume/stop + speechRate (0.5–2). |
Props API#
Public surface of A11yToolbar. Theme and i18n are nested props.
A11yToolbar#
Core open state, storage, shortcuts/hotkey, and feature flags.
A11yToolbarProps
| Prop | Type | Default | Description |
|---|---|---|---|
| storageKey | string | "itzsa-a11y" | localStorage key for preferences. Locale is stored at {storageKey}:locale when uncontrolled. |
| defaultOpen | boolean | false | Uncontrolled initial open state of the panel. |
| open | boolean | - | Controlled open state. Pair with onOpenChange. |
| onOpenChange | (open: boolean) => void | - | Called when the panel should open or close. |
| features | Partial<Record<FeatureId, boolean>> | all on | Set a feature id to false to hide that control from the panel. |
| hotkey | { altKey?, ctrlKey?, metaKey?, shiftKey?, key } | null | { altKey: true, key: 'a' } | Panel toggle shortcut. Synced into the shortcuts registry when shortcuts is omitted. Pass null to disable the panel binding. |
| shortcuts | A11yShortcutDef[] | false | DEFAULT_A11Y_SHORTCUTS | Full keyboard map (panel + features). false = panel hotkey only. Use mergeA11yShortcuts to override by id. Scalable — add bindings without forking UI. |
| onChange | (prefs: A11yPreferences) => void | - | Fires after preference updates (also used for host sync). |
| launcherLabel | string | locale message | Accessible name for the floating launcher. Defaults to the active locale’s launcherLabel. |
| className | string | - | Extra class on the toolbar root. |
| style | CSSProperties | - | Inline styles merged onto the toolbar root (after theme vars). |
| accentColor | string | - | Deprecated. Prefer theme.accent / theme.launcher. |
theme & styles#
Chrome and effect tokens via theme, style, or host CSS variables. Header pair defaults clear ~4.9:1.
A11yToolbarTheme + style
| Prop | Type | Default | Description |
|---|---|---|---|
| accent | string | "#1d9e75" | Primary accent — icons, steps, pressed borders. |
| header | string | "#15805f" | Header background. Default is darker than brand accent so white text clears ~4.9:1. |
| headerForeground | string | "#ffffff" | Header title + icon button color. Do not pair white with #1d9e75 (~3.4:1). |
| icon | string | accent | Card / panel icon + step fill. |
| focusRing | string | "#0b3d34" | Focus ring for launcher, cards, header controls (non-text ≥3:1 intent). |
| background / card / foreground / muted | string | - | Panel shell, feature cards, body text, secondary labels. |
| border / shadow / radius / zIndex | string | number | - | Chrome geometry and stacking. |
| fontFamily | string | - | Force one font stack for all locales. Prefer fontFamilyByLocale when en/ne differ. |
| fontFamilyByLocale | Record<string, string> | - | Per-locale fonts. Defaults: en → Outfit, ne → Poppins (+ Devanagari fallbacks). |
| launcher / launcherForeground / launcherRing / launcherRadius | string | - | Floating launcher fill, glyph, ring, and corner radius. |
| cursor | string | - | Bigger-cursor CSS value, e.g. url("…") 2 2. Synced onto <html> so it applies page-wide when the preference is on. |
| guideHeight | string | - | Reading-guide band height (e.g. "48px"). |
| style (prop) | CSSProperties | - | Pass any --itzsa-a11y-* custom property via CSS_VAR helpers, or set the same vars on :root in host CSS. |
import { A11yToolbar, CSS_VAR } from "@itzsa/a11y-toolbar";
// 1) theme prop — each token maps to --itzsa-a11y-*
<A11yToolbar
theme={{
accent: "var(--accent)",
header: "#15805f",
headerForeground: "#ffffff",
background: "#e8eaef",
card: "#f7f6f4",
foreground: "#1a1a1a",
muted: "#4b4b4b",
radius: "8px",
launcherRadius: "999px",
// Bigger cursor asset (applied on <html> when preference is on)
cursor: 'url("/cursors/big.svg") 2 2',
guideHeight: "56px",
}}
/>
// 2) style prop — same CSS variables, any token
<A11yToolbar
style={{
[CSS_VAR.toolbarAccent]: "#0f766e",
[CSS_VAR.toolbarRadius]: "12px",
[CSS_VAR.launcherSize]: "3.25rem",
}}
/>
// 3) Host stylesheet — override on :root / html
/*
:root {
--itzsa-a11y-toolbar-accent: #0f766e;
--itzsa-a11y-cursor: url("/cursors/big.svg") 2 2;
}
*/Bigger cursor asset
theme.cursor is written onto <html> so the larger pointer applies page-wide as soon as the preference is on (including while the panel is open).i18n#
English by default. Resolution: en → locales[active] → messages. Panel sets lang={locale}.
Locale props
| Prop | Type | Default | Description |
|---|---|---|---|
| locale | string | - | Controlled locale (sync with Zustand / Redux / next-intl). Host owns persistence when set. |
| defaultLocale | string | "en" | Uncontrolled initial locale. |
| onLocaleChange | (locale: string) => void | - | Fires when the user picks a language (and for host sync). |
| messages | A11yMessagesPartial | - | Deep-partial overrides merged last (on top of the active locale dictionary). |
| locales | Record<string, A11yMessagesPartial> | - | Extra dictionaries. Built-in en is always available; missing keys fall back to English. Ship NE_MESSAGES for full Nepali. |
| availableLocales | string[] | en + keys(locales) | Codes offered in the language switcher. Switcher appears when length > 1. |
// Controlled locale — same source of truth as Zustand / Redux / next-intl
const locale = useAppLocale();
const setLocale = useSetAppLocale();
<A11yToolbar
locale={locale}
onLocaleChange={setLocale}
locales={{ ne: NE_MESSAGES }}
messages={{ panelTitle: "Site accessibility" }}
/>Fonts
Defaults: English → Outfit, Nepali → Poppins, with Noto Sans Devanagari fallbacks so names like नेपाली render. Override withfontFamily or fontFamilyByLocale. Read Aloud uses ne-NP and picks an installed Nepali voice (Hindi Devanagari fallback).Placement & size#
position places the launcher; panelAlign places the panel horizontally. By default the panel stretches edge-to-edge (launcher floats on top — no gap above the icon).
Placement
| Prop | Type | Default | Description |
|---|---|---|---|
| position | A11yToolbarPosition | "bottom-right" | Launcher placement: bottom-right | bottom-left | top-right | top-left | bottom-center | top-center | middle-left | middle-right. |
| panelAlign | "auto" | "left" | "right" | "center" | "auto" | Horizontal panel edge, independent of the launcher. auto follows the icon; left / right / center override. |
| offset | string | "1.25rem" | Gap from the viewport edge (CSS length). |
| launcherSize | string | "3.5rem" | Floating launcher button size. |
| panelMaxHeight | string | none | Panel max height ceiling, e.g. "32rem" or "70dvh". Default none (full stretch). |
| panelHeight | string | — (stretch) | Optional fixed height. When unset, the panel stretches between safe edges and the launcher. |
Web accessibility (WCAG)#
The toolbar chrome follows WCAG 2.2 POUR principles and related success criteria. It does not certify the host site.
Principle first
WCAG is organized as POUR — Perceivable, Operable, Understandable, Robust. Preference controls map to those principles; semantic HTML and keyboard paths in your app remain required.POUR mapping
| Prop | Type | Default | Description |
|---|---|---|---|
| Perceivable | POUR | - | Text size, contrast, color filters, saturation, hide images, highlight links, bigger cursor, reading guide, read aloud — presentation / assist alternatives without removing content. |
| Operable | POUR | - | Keyboard shortcuts, focus trap, Esc to close, real buttons, pause animations (also respects prefers-reduced-motion). |
| Understandable | POUR | - | Panel lang={locale} (3.1.2), live-region announcements for level/toggle changes, predictable reset. |
| Robust | POUR | - | ARIA dialog / toggle patterns (APG), versioned storage schema, FOUC attrs before paint so AT and CSS see the same state. |
Success criteria the chrome targets
| Prop | Type | Default | Description |
|---|---|---|---|
| 1.4.3 / 1.4.11 Contrast | Level AA | - | Default header pair targets ≥4.5:1 text; focus rings target ≥3:1 non-text. Hosts must verify launcher ring on their background. |
| 1.4.12 Text spacing | Level AA | - | Max letter / word / line presets meet WCAG spacing floors (see effect-values.ts). |
| 2.1.1 Keyboard | Level A | - | All controls are buttons; shortcuts skip editable fields; panel closes with Escape. |
| 2.2.2 Pause / 2.3.3 Motion | Level A / AAA | - | Pause Animations kills transitions/animations under the content root; additive with prefers-reduced-motion. |
| 2.4.7 Focus Visible | Level AA | - | Visible focus ring on launcher, cards, and header controls. |
| 3.1.2 Language of Parts | Level AA | - | Dialog sets lang to the active locale when i18n is used. |
| 4.1.2 Name, Role, Value | Level A | - | Launcher exposes expanded/controls; toggles use aria-pressed; dialog labelled by title. |
What this package is not
Enabling the toolbar does not make an inaccessible page WCAG-compliant. Use it as visitor preference chrome alongside a sound base UI — headings, labels, focus order, and contrast in your own components.Behavior & patterns#
APG-aligned dialog / toggle behavior. See package BEHAVIOR.md for control-level detail.
Behavior
| Prop | Type | Default | Description |
|---|---|---|---|
| Dialog | pattern | - | role=dialog, aria-modal, labelled title, focus trap, Esc to close, restore focus. |
| Launcher | pattern | - | aria-haspopup, aria-expanded, aria-controls, accessible name. |
| Cards | pattern | - | Real <button>s; toggles use aria-pressed; stepped levels announced via aria-live. |
| Language | pattern | - | Panel sets lang={locale} (WCAG 3.1.2). Switcher has a visually hidden label; change announces in the new locale. |
| Scope | architecture | - | Effects apply under [data-a11y-content] only. Mount toolbar outside that root. |
| Disclaimer | product | - | WCAG-grounded chrome — does not make an inaccessible host site WCAG-compliant. |
Known risks
- Default hotkey Alt+A may clash with screen readers — remap or disable as needed.
- Stepped cards use dash indicators; full level names live in the accessible name + live region.
- Launcher focus-ring contrast on dark host backgrounds is host-dependent — verify in your theme.
Headless / FOUC#
Server-safe helpers live on @itzsa/a11y-toolbar/headless — do not import the React entry from RSC layouts.
Headless exports
| Prop | Type | Default | Description |
|---|---|---|---|
| getA11yFoucScript | (storageKey?) => string | - | Inline <head> script: restores prefs + data-a11y-locale before paint. Import from /headless in RSC. |
| applyA11yPreferences | (prefs, options?) => void | - | Apply attrs + CSS vars to the root (debounced helpers also exported). |
| NE_MESSAGES | A11yMessages | - | Complete Nepali dictionary for locales={{ ne: NE_MESSAGES }}. |
| resolveMessages | (options) => A11yMessages | - | Merge en → locales[active] → messages. Headless-safe. |
| EN_MESSAGES | A11yMessages | - | Built-in English dictionary (merge base). |
FOUC
Prefs attrs anddata-a11y-locale are set before paint. Translated chrome strings resolve in React — for SSR-safe locale copy, pass controlled locale from the host.WordPress / CDN#
Minified IIFE with React bundled — use on classic WordPress, Webflow, or any static HTML site without a React build.
window.ItzsaA11yToolbar
| Prop | Type | Default | Description |
|---|---|---|---|
| mount | (options?) => HTMLElement | - | Mount the toolbar. Options = A11yToolbar props plus target / contentRoot. React is bundled in the min file. |
| unmount | () => void | - | Tear down a previous mount(). |
| getA11yFoucScript | (storageKey?) => string | - | Inline <head> script string for FOUC prevention. |
| NE_MESSAGES / EN_MESSAGES | A11yMessages | - | Locale dictionaries for locales: { ne: ItzsaA11yToolbar.NE_MESSAGES }. |
| DEFAULT_A11Y_SHORTCUTS / mergeA11yShortcuts | registry helpers | - | Same shortcut map as the React API — pass shortcuts: ItzsaA11yToolbar.mergeA11yShortcuts(...) in mount(). |
| options.target | string | HTMLElement | - | Mount node or selector. Default: append a host div to document.body. |
| options.contentRoot | true | string | HTMLElement | - | Ensure data-a11y-content exists. true → document.body; or pass a selector/element. |
Hosted on this docs site — copy a URL and paste into <link> / <script>.
CSS
https://itzsa.acharya-suman.com.np/cdn/a11y-toolbar/a11y-toolbar.min.css
JS
https://itzsa.acharya-suman.com.np/cdn/a11y-toolbar/a11y-toolbar.min.js
After the package is on npm, jsDelivr mirrors dist/ (pin a version in production instead of @latest).
jsDelivr CSS
https://cdn.jsdelivr.net/npm/@itzsa/a11y-toolbar@latest/dist/a11y-toolbar.min.css
jsDelivr JS
https://cdn.jsdelivr.net/npm/@itzsa/a11y-toolbar@latest/dist/a11y-toolbar.min.js
<!-- itzsa CDN (hosted on docs site) -->
<link rel="stylesheet" href="https://itzsa.acharya-suman.com.np/cdn/a11y-toolbar/a11y-toolbar.min.css" />
<script src="https://itzsa.acharya-suman.com.np/cdn/a11y-toolbar/a11y-toolbar.min.js"></script>
<script>
ItzsaA11yToolbar.mount({
position: "bottom-center",
panelAlign: "left",
contentRoot: "main", // or true for <body>
locales: { ne: ItzsaA11yToolbar.NE_MESSAGES },
// Optional: remap shortcuts
// shortcuts: ItzsaA11yToolbar.mergeA11yShortcuts(
// ItzsaA11yToolbar.DEFAULT_A11Y_SHORTCUTS,
// [{ id: "reset", keys: null }],
// ),
});
</script>add_action('wp_enqueue_scripts', function () {
$css = 'https://itzsa.acharya-suman.com.np/cdn/a11y-toolbar/a11y-toolbar.min.css';
$js = 'https://itzsa.acharya-suman.com.np/cdn/a11y-toolbar/a11y-toolbar.min.js';
wp_enqueue_style('itzsa-a11y', $css, [], null);
wp_enqueue_script('itzsa-a11y', $js, [], null, true);
wp_add_inline_script('itzsa-a11y', 'ItzsaA11yToolbar.mount({ contentRoot: "main" });', 'after');
});Content root
Put your page content in an element withdata-a11y-content, or pass contentRoot: true to stamp it on <body>. Prefer printing getA11yFoucScript() in <head> to avoid preference flash.