Vanilla JS / HTML
Overview#
Add AccessPath to any website by dropping in a single script tag. It works on WordPress, Shopify, a static HTML site, or anywhere you can paste a snippet of HTML. The script reads its own data-* attributes, mounts a floating trigger and drawer inside a Shadow DOM root, and injects both core stylesheets automatically, so there is no separate CSS file to include.
Wrap the part of the page you want AccessPath to affect in a container element. It defaults to <body>, or you can point it at a different element with data-target. This container is what receives the a11y-* effect classes. The panel and floating trigger always mount at the document root, outside that container, so effects like saturation or color inversion never visually apply to the panel's own interface.
Installation#
<script src="https://cdn.jsdelivr.net/npm/@accesspath/embed@0/dist/embed.js"
data-theme="light"
data-storage-key="accesspath-prefs"
data-position="bottom-right"
data-shape="circle"
data-icon="accessibility"
data-draggable="true"
data-brand="#4928F3">
</script>
Loads from jsDelivr, pinned to @0 so a future major release can't change it under you. To self-host instead, install @accesspath/embed (or download dist/embed.js) and point src at your own copy.
data-* attributes#
| Attribute | Values | Default | Does |
|---|---|---|---|
data-theme | light / dark | light | Drawer chrome theme. |
data-storage-key | any string | accesspath-prefs | localStorage key; instances sharing a key share state. |
data-target | CSS selector | document.body | Element the a11y-* classes apply to. |
data-position | bottom-right / bottom-left / top-right / top-left | bottom-right | Corner the floating trigger docks to. |
data-shape | circle / rounded-square / pill | circle | Trigger button shape. |
data-icon | accessibility / motion / contrast / spacing / motor / badge / logo | accessibility | Trigger button icon. If you need a custom SVG icon, use the core, React, or Angular integration instead, since a DOM element cannot travel through a data attribute. |
data-draggable | true / false | false | Lets the visitor drag the trigger; its dropped position is remembered per storageKey. |
data-brand | hex color | AccessPath violet | Overrides the --ap-brand-* token set. |
data-profiles | comma list of profile keys | all 6 | Restricts which preset profile pills render. |
data-sections | comma list: profiles,quick,controls,actions | all 4, that order | Which top-level drawer sections render, and in what order. |
data-control-categories | comma list: vision,content,motion,reading,navigation | all 5 | Which built-in category groups render inside "controls". |
data-locale | en / es / fr / de / pt | en | Bundled translation set for every panel string. |
data-labels | JSON object | None | Per-string overrides layered on top of data-locale. Same nested shape as the Labels type. |
data-actions | JSON array of {"id","label","icon?","ariaLabel?"} | None | Custom buttons in the drawer's Actions section. |
data-hide-trigger | true / false | false | Skips the floating trigger. Open the drawer from your own button via window.AccessPath below. |
Scripting & events#
The embed exposes one page-global for hosts that want to open the drawer from their own UI, or listen for custom action clicks:
window.AccessPath.open();
window.AccessPath.close();
window.AccessPath.toggle();
// data-actions='[{"id":"support","label":"Contact support"}]'
document.body.addEventListener('accesspath:action', (e) => {
if (e.detail.id === 'support') { /* ... */ }
});
The event fires on whatever element data-target resolves to, which is document.body by default. Listen for it there, not on window.AccessPath itself.
React
Overview#
The React integration ships from @accesspath/core, which owns all state through AccessPathState and builds the panel's DOM. <AccessPathPanel> is a thin wrapper around that shared core.
Wrap the part of the page you want AccessPath to affect in a container using the container prop, which defaults to document.body. This is what receives the a11y-* effect classes. The panel and floating trigger always mount at document.documentElement, outside that container, so effects like saturation or color inversion never visually apply to the panel's own interface.
Installation#
npm install @accesspath/react
import { AccessPathPanel } from '@accesspath/react';
import '@accesspath/core/styles/a11y-effects.css';
import '@accesspath/core/styles/panel.css';
function App() {
const containerRef = useRef<HTMLDivElement>(null);
return (
<div ref={containerRef} className="a11y-target">
<AccessPathPanel container={containerRef.current} storageKey="accesspath-prefs" />
{/* rest of your app */}
</div>
);
}
<AccessPathPanel> renders nothing itself, since it simply returns null. It creates the panel once on mount and appends it to document.documentElement, outside your component tree, so it is never a descendant of container.
Component props#
| Prop | Type | Default | Does |
|---|---|---|---|
container | HTMLElement | null | document.body | Element the a11y-* classes apply to. |
isDarkTheme | boolean | false | Drawer chrome theme. Live-updatable after mount. |
storageKey | string | a11y-prefs | localStorage key; instances sharing a key share state. |
profiles | ProfileKey[] | all 6 | Restricts which preset profile buttons render. |
brandColor | string | AccessPath violet | Overrides the --ap-brand-* token set. |
actions | CustomActionConfig[] | None | Custom buttons in the drawer's Actions section. |
onAction | (id: string) => void | None | Fires when a custom action button is clicked. Sugar over listening for accesspath:action on container yourself. |
locale | LocaleKey | en | Bundled translation set. |
labels | LabelOverrides | None | Per-string overrides on top of locale. |
sections | SectionKey[] | ['profiles','quick','controls','actions'] | Top-level sections rendered, and their order. |
controlCategories | ControlCategoryKey[] | all 5 | Which built-in category groups render inside "controls". |
customSections | CustomSectionConfig[] | None | Host-supplied sections with a render(el) callback, same chrome as built-ins. |
profiles, storageKey, actions, sections, controlCategories, and customSections are only read once, at mount time. The panel is not recreated if these change identity later. Only isDarkTheme is live-bindable.
Imperative handle (ref)#
const panelRef = useRef<AccessPathPanelHandle>(null);
<AccessPathPanel ref={panelRef} />
panelRef.current?.open();
panelRef.current?.close();
panelRef.current?.reset();
useAccessPath() hook#
For driving the panel from your own UI (e.g. a nav bar button, a settings page) without mounting <AccessPathPanel> at all, or reading live prefs to adapt your own components:
import { useAccessPath } from '@accesspath/react';
function MyToolbarButton() {
const { open, isOpen, prefs, activeProfiles } = useAccessPath('accesspath-prefs');
return <button onClick={open}>Accessibility{isOpen ? ' (open)' : ''}</button>;
}
Pass the same storageKey you used for <AccessPathPanel> so both read/write the same shared AccessPathState instance. Returns open(), close(), reset(), isOpen, prefs (the full A11yPrefs object), and activeProfiles (more than one profile can be active at once).
Angular
Overview#
The Angular integration ships from @accesspath/core, which owns all state through AccessPathState and builds the panel's DOM. AccessibilityPanelComponent is a thin wrapper around that shared core.
Wrap the part of the page you want AccessPath to affect in a container using the container input, which defaults to document.body. This is what receives the a11y-* effect classes. The panel and floating trigger always mount at document.documentElement, outside that container, so effects like saturation or color inversion never visually apply to the panel's own interface.
Installation#
npm install @accesspath/angular
import { AccessibilityPanelComponent } from '@accesspath/angular';
@Component({
standalone: true,
imports: [AccessibilityPanelComponent],
template: `
<div #root class="a11y-target">
<app-accessibility-panel
[container]="root"
storageKey="accesspath-prefs"
(action)="onAction($event)">
</app-accessibility-panel>
<!-- rest of your app -->
</div>
`,
})
export class AppComponent {
onAction(id: string) { /* ... */ }
}
AccessibilityPanelComponent is a standalone component with an empty template. It renders nothing into the DOM tree itself, and instead appends the real panel to document.documentElement inside ngOnInit, the same mount-outside-container pattern used by the React and embed integrations.
Inputs#
| Input | Type | Default | Does |
|---|---|---|---|
container | HTMLElement | document.body | Element the a11y-* classes apply to. |
isDarkTheme | boolean | false | Drawer chrome theme. Live-updatable after init. |
storageKey | string | a11y-prefs | localStorage key; instances sharing a key share state. |
profiles | ProfileKey[] | all 6 | Restricts which preset profile buttons render. |
brandColor | string | AccessPath violet | Overrides the --ap-brand-* token set. Set once, at init. |
actions | CustomActionConfig[] | None | Custom buttons in the drawer's Actions section. |
locale | LocaleKey | en | Bundled translation set. |
labels | LabelOverrides | None | Per-string overrides on top of locale. |
sections | SectionKey[] | ['profiles','quick','controls','actions'] | Top-level sections rendered, and their order. |
controlCategories | ControlCategoryKey[] | all 5 | Which built-in category groups render inside "controls". |
customSections | CustomSectionConfig[] | None | Host-supplied sections with a render(el) callback, same chrome as built-ins. |
The same one-shot rule from React applies here too. Everything except isDarkTheme is only read inside ngOnInit, so changing profiles, storageKey, or the other inputs later will not recreate the panel.
Output & public methods#
| Output | Payload | Fires when |
|---|---|---|
action | string (the action's id) | A custom action button is clicked. |
Grab a template reference to call these methods directly. It plays the same role that ref plays in React:
<app-accessibility-panel #panel [container]="root"></app-accessibility-panel>
<button (click)="panel.open()">Open</button>
| Method | Does |
|---|---|
open() | Opens the drawer. |
close() | Closes the drawer. |
reset() | Resets all preferences and active profiles to defaults. |
Shared config reference
These types are shared across all three integrations, whether you set them as data-* attributes on the embed or pass them directly as props and inputs on React and Angular.
Architecture#
All three integrations ship from @accesspath/core, which owns all state through AccessPathState and builds the panel's DOM with createPanel(). @accesspath/embed, @accesspath/react, and @accesspath/angular are thin wrappers around that same core, so they offer the same options and the same behavior, just a different surface for wiring them up. Two instances that share a storageKey automatically share state, even across different frameworks on the same page.
Profiles (ProfileKey)#
| Key | Label | Turns on |
|---|---|---|
low-vision | Low Vision | Larger text, pause animations, higher saturation |
dyslexia | Dyslexia | Dyslexia-friendly font, wider spacing, taller line height |
seizure | Seizure Safe | Pause animations, reduced saturation |
motor | Motor Impaired | Larger text, pause animations |
colorblind | Color Blind | Desaturated colors, higher contrast, wider spacing |
adhd | ADHD | Pause animations, wider spacing |
Sections & control categories#
SectionKey accepts 'profiles', 'quick', 'controls', or 'actions', or a custom id from customSections. Custom section ids only work with the core, React, or Angular integration, since the embed can't carry a render callback through a data attribute.
ControlCategoryKey accepts 'vision', 'content', 'motion', 'reading', or 'navigation', the 5 collapsible groups inside the "controls" section.
Locales (LocaleKey)#
'en' | 'es' | 'fr' | 'de' | 'pt'. For any other language, use labels / data-labels to override every string yourself.
Theming tokens#
All colors read from namespaced --ap-* CSS custom properties, never bare tokens like --brand, so AccessPath never collides with a host page's own design system. Set them on :root, or pass brandColor or data-brand for a quick one-line override of just the brand color. You can find the full token table on the home page's Customize tab.
Where state lives#
AccessPath never sends anything to a server. Every accessibility preference is saved to the visitor's own browser storage under storageKey, and it loads back automatically the next time they visit your site. Read the Privacy Policy and the Accessibility Guide for the full picture of what this web accessibility widget does, and does not, do.
Known limitations#
Safari/WebKit keyboard navigation. By default, Safari only lets the Tab key reach text fields and dropdowns, skipping plain buttons entirely — a Safari-wide setting (System Settings → Keyboard → "Full Keyboard Access", off by default), not something any individual website, including this one, can turn on for a visitor. With it off, Tab won't reach most of the panel's buttons in Safari specifically; every other browser tabs through the whole panel normally, and Escape/focus-return work correctly everywhere once focus is actually inside the panel. Visitors who rely on the keyboard in Safari should enable Full Keyboard Access once, which fixes this for every button-based site they use, not just this one.