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).

@itzsa/a11y-toolbarReact 18 / 19i18n · FOUC

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-toolbar

Peers

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.

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 respect features 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

PropTypeDefaultDescription
textSizestepped (0–3)-Content zoom (112.5% / 125% / 145%). Uses CSS zoom when supported.
textSpacingstepped (0–2)-Letter + word spacing. Max meets WCAG 1.4.12 floors.
lineHeightstepped (0–2)-Line height 1.5 / 1.75 / 2 (all ≥ 1.5).
fontSelectionstepped (0–2)-Default / system UI / readable serif on content.
textAlignstepped (0–2)-Left / center / right.
dyslexiaFriendlytoggle-Spacing-only reading aid (max spacing + line height). No bundled font in v1.
highContraststepped (0–2)-Stronger text/UI contrast on content.
colorFilterstepped (0–3)-Grayscale / hue / sepia presentation aids — not clinical CVD tools.
saturationstepped (0–2)-Full / reduced / none color saturation.
hideImagestoggle-Hides photos/media via visibility; keeps labeled icons in controls.
highlightLinkstoggle-Emphasize anchors for low-vision scanning.

Motion & assist#

Motion reduction and pointer reading aids.

Motion & assist

PropTypeDefaultDescription
pauseAnimationstoggle-Stops animation, transition, and scroll-behavior on content. Additive with prefers-reduced-motion.
biggerCursortoggle-32×32 SVG cursor (keyword fallback auto). Applies page-wide immediately — including over the open panel/overlay.
readingGuidetoggle-Horizontal reading band that follows the pointer.
readAloudtoggle-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

PropTypeDefaultDescription
storageKeystring"itzsa-a11y"localStorage key for preferences. Locale is stored at {storageKey}:locale when uncontrolled.
defaultOpenbooleanfalseUncontrolled initial open state of the panel.
openboolean-Controlled open state. Pair with onOpenChange.
onOpenChange(open: boolean) => void-Called when the panel should open or close.
featuresPartial<Record<FeatureId, boolean>>all onSet 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.
shortcutsA11yShortcutDef[] | falseDEFAULT_A11Y_SHORTCUTSFull 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).
launcherLabelstringlocale messageAccessible name for the floating launcher. Defaults to the active locale’s launcherLabel.
classNamestring-Extra class on the toolbar root.
styleCSSProperties-Inline styles merged onto the toolbar root (after theme vars).
accentColorstring-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

PropTypeDefaultDescription
accentstring"#1d9e75"Primary accent — icons, steps, pressed borders.
headerstring"#15805f"Header background. Default is darker than brand accent so white text clears ~4.9:1.
headerForegroundstring"#ffffff"Header title + icon button color. Do not pair white with #1d9e75 (~3.4:1).
iconstringaccentCard / panel icon + step fill.
focusRingstring"#0b3d34"Focus ring for launcher, cards, header controls (non-text ≥3:1 intent).
background / card / foreground / mutedstring-Panel shell, feature cards, body text, secondary labels.
border / shadow / radius / zIndexstring | number-Chrome geometry and stacking.
fontFamilystring-Force one font stack for all locales. Prefer fontFamilyByLocale when en/ne differ.
fontFamilyByLocaleRecord<string, string>-Per-locale fonts. Defaults: en → Outfit, ne → Poppins (+ Devanagari fallbacks).
launcher / launcherForeground / launcherRing / launcherRadiusstring-Floating launcher fill, glyph, ring, and corner radius.
cursorstring-Bigger-cursor CSS value, e.g. url("…") 2 2. Synced onto <html> so it applies page-wide when the preference is on.
guideHeightstring-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

PropTypeDefaultDescription
localestring-Controlled locale (sync with Zustand / Redux / next-intl). Host owns persistence when set.
defaultLocalestring"en"Uncontrolled initial locale.
onLocaleChange(locale: string) => void-Fires when the user picks a language (and for host sync).
messagesA11yMessagesPartial-Deep-partial overrides merged last (on top of the active locale dictionary).
localesRecord<string, A11yMessagesPartial>-Extra dictionaries. Built-in en is always available; missing keys fall back to English. Ship NE_MESSAGES for full Nepali.
availableLocalesstring[]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 with fontFamily 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

PropTypeDefaultDescription
positionA11yToolbarPosition"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.
offsetstring"1.25rem"Gap from the viewport edge (CSS length).
launcherSizestring"3.5rem"Floating launcher button size.
panelMaxHeightstringnonePanel max height ceiling, e.g. "32rem" or "70dvh". Default none (full stretch).
panelHeightstring— (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

PropTypeDefaultDescription
PerceivablePOUR-Text size, contrast, color filters, saturation, hide images, highlight links, bigger cursor, reading guide, read aloud — presentation / assist alternatives without removing content.
OperablePOUR-Keyboard shortcuts, focus trap, Esc to close, real buttons, pause animations (also respects prefers-reduced-motion).
UnderstandablePOUR-Panel lang={locale} (3.1.2), live-region announcements for level/toggle changes, predictable reset.
RobustPOUR-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

PropTypeDefaultDescription
1.4.3 / 1.4.11 ContrastLevel 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 spacingLevel AA-Max letter / word / line presets meet WCAG spacing floors (see effect-values.ts).
2.1.1 KeyboardLevel A-All controls are buttons; shortcuts skip editable fields; panel closes with Escape.
2.2.2 Pause / 2.3.3 MotionLevel A / AAA-Pause Animations kills transitions/animations under the content root; additive with prefers-reduced-motion.
2.4.7 Focus VisibleLevel AA-Visible focus ring on launcher, cards, and header controls.
3.1.2 Language of PartsLevel AA-Dialog sets lang to the active locale when i18n is used.
4.1.2 Name, Role, ValueLevel 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

PropTypeDefaultDescription
Dialogpattern-role=dialog, aria-modal, labelled title, focus trap, Esc to close, restore focus.
Launcherpattern-aria-haspopup, aria-expanded, aria-controls, accessible name.
Cardspattern-Real <button>s; toggles use aria-pressed; stepped levels announced via aria-live.
Languagepattern-Panel sets lang={locale} (WCAG 3.1.2). Switcher has a visually hidden label; change announces in the new locale.
Scopearchitecture-Effects apply under [data-a11y-content] only. Mount toolbar outside that root.
Disclaimerproduct-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

PropTypeDefaultDescription
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_MESSAGESA11yMessages-Complete Nepali dictionary for locales={{ ne: NE_MESSAGES }}.
resolveMessages(options) => A11yMessages-Merge en → locales[active] → messages. Headless-safe.
EN_MESSAGESA11yMessages-Built-in English dictionary (merge base).

FOUC

Prefs attrs and data-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

PropTypeDefaultDescription
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_MESSAGESA11yMessages-Locale dictionaries for locales: { ne: ItzsaA11yToolbar.NE_MESSAGES }.
DEFAULT_A11Y_SHORTCUTS / mergeA11yShortcutsregistry helpers-Same shortcut map as the React API — pass shortcuts: ItzsaA11yToolbar.mergeA11yShortcuts(...) in mount().
options.targetstring | HTMLElement-Mount node or selector. Default: append a host div to document.body.
options.contentRoottrue | 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 with data-a11y-content, or pass contentRoot: true to stamp it on <body>. Prefer printing getA11yFoucScript() in <head> to avoid preference flash.