@leapdev/a11y-sdk
v0.1.0
Published
Framework-agnostic browser accessibility utilities.
Maintainers
Keywords
Readme
LEAP Accessibility SDK
A framework-agnostic, browser-focused TypeScript package for LEAP accessibility functionality.
Package structure
The SDK exposes each feature through a separate package subpath:
import {} from '@leap/a11y-sdk/font-scaling';
import {} from '@leap/a11y-sdk/font-selection';
import {} from '@leap/a11y-sdk/highlight-focus';
import {} from '@leap/a11y-sdk/reading-guide';All four feature modules are available for use.
There is deliberately no root entry point. Consumers import only the feature they need, and the package declares that its modules have no side effects so bundlers can remove unused code.
Install from the distributed tarball
The SDK is distributed as a .tgz package. Consumers do not need to clone or
build this repository.
Requirements
The consuming application must provide:
- A modern browser environment.
- An ESM-compatible bundler.
- TypeScript when consuming the included declarations.
- Yarn or npm.
Verify the archive
If a checksum was supplied with the tarball, obtain it through a trusted channel separate from the archive and verify it before installation.
On macOS:
shasum -a 256 ./leap-a11y-sdk.tgzOn Linux:
sha256sum ./leap-a11y-sdk.tgzOn PowerShell:
Get-FileHash .\leap-a11y-sdk.tgz -Algorithm SHA256Confirm that the result matches the separately supplied checksum.
Install with Yarn
From the consuming application's root directory:
yarn add ./path/to/leap-a11y-sdk.tgzInstall with npm
npm install ./path/to/leap-a11y-sdk.tgzThe package is installed as @leap/a11y-sdk.
Keep the tarball available
The package manager records the archive as a file: dependency. The file must
remain available whenever dependencies are installed.
For temporary evaluation, keep the dependency changes local. For committed usage, use a stable path available to developers and CI, such as:
vendor/leap-a11y-sdk.tgzInstall the archive from that location:
yarn add ./vendor/leap-a11y-sdk.tgzFollow the consuming repository's policy before committing binary package archives.
Verify the installation
Run the consuming application's normal checks after installation. Script names vary by repository; for example:
yarn install
yarn typecheck
yarn buildConfirm that Yarn resolved the package:
yarn why @leap/a11y-sdkWith npm:
npm ls @leap/a11y-sdkImport a feature
Import the required feature through its package subpath:
import { createFontScaling } from '@leap/a11y-sdk/font-scaling';
import { createFontSelection } from '@leap/a11y-sdk/font-selection';
import { createHighlightFocus } from '@leap/a11y-sdk/highlight-focus';
import { createReadingGuide } from '@leap/a11y-sdk/reading-guide';The package root does not export feature APIs. This import will not work:
import { createHighlightFocus } from '@leap/a11y-sdk';Basic usage
import { createHighlightFocus } from '@leap/a11y-sdk/highlight-focus';
const highlightFocus = createHighlightFocus();
highlightFocus.enable();
// Destroy the controller when its owning application or view is torn down.
highlightFocus.destroy();The SDK does not initialise features automatically. The consuming application is responsible for:
- Creating controllers in a browser context.
- Enabling and configuring features.
- Connecting feature flags and persisted preferences.
- Passing application-specific selectors and exclusions.
- Destroying controllers during application, view, or iframe teardown.
Applications using server-side rendering must not create controllers on the
server. Create them from a browser-only lifecycle hook after the target
document has its required head, body, and defaultView, or after the
owning framework view has mounted.
Font scaling, font selection, and highlight focus require a controller for each target iframe document. Parent code can create those controllers only for same-origin iframe documents; otherwise, initialise the SDK from code running inside the iframe.
Reading guide is different: one parent controller renders the host overlay, and
iframe pointer movement must be forwarded through the documented
postMessage contract. Creating a reading-guide controller inside an iframe
renders a separate local guide and does not provide the sender.
Update the tarball
When a new archive is supplied, remove the existing file dependency and install the new archive:
yarn remove @leap/a11y-sdk
yarn add ./path/to/leap-a11y-sdk.tgzWith npm:
npm uninstall @leap/a11y-sdk
npm install ./path/to/leap-a11y-sdk.tgzCommit the resulting package.json and lockfile changes only when the tarball
location is stable and available to CI. CI can then use its normal immutable
install command, such as yarn install --immutable or npm ci.
Troubleshooting
Package subpath is not exported
Import a documented feature subpath rather than the package root:
import { createReadingGuide } from '@leap/a11y-sdk/reading-guide';The SDK is ESM-only and does not support CommonJS require(). If a valid
subpath still fails, confirm that the consumer emits ESM and uses a compatible
TypeScript module resolution mode such as Bundler, Node16, or NodeNext.
document is undefined
The controller is being created outside a browser context. Move controller creation into the application's browser lifecycle after the target document has the DOM structures required by that feature.
CI cannot find the tarball
The file: dependency points to a path that is unavailable in CI. Move the
archive to a stable repository-relative location or distribute it through the
approved internal package registry.
An updated tarball behaves like the previous version
Remove and reinstall the dependency so the package manager recalculates the archive checksum:
yarn remove @leap/a11y-sdk
yarn add ./path/to/leap-a11y-sdk.tgzWith npm:
npm uninstall @leap/a11y-sdk
npm install ./path/to/leap-a11y-sdk.tgzFont scaling
Font scaling reproduces the Sirius inline font-size scaling algorithm while keeping application-specific selectors in the consumer:
import { createFontScaling } from '@leap/a11y-sdk/font-scaling';
const fontScaling = createFontScaling({
targets: {
selectors: [
'a',
'button',
'input',
'select',
'textarea',
'label',
'p',
'li',
'h1',
'h2',
'h3',
],
excludeSelectors: [
'#a11y-widget',
'#a11y-reading-guide',
'#a11y-live',
'sc-icon',
'.x-icon',
'[class*="icon"]',
],
},
});
fontScaling.setScale(1.25);
fontScaling.getScale();
fontScaling.reset();
fontScaling.destroy();Scaling defaults to the Sirius bounds of 0.85 through 1.75. Values outside
the bounds are clamped, and non-finite values reset the scale to 1. Custom
bounds must be finite, positive, ordered, and include 1. Values less than
0.001 away from 1 normalize to 1 and trigger a full reset.
The controller records each matched element's original computed and inline font
size before applying originalSize * scale as an inline pixel value. Reset and
destroy restore tracked elements even if they have been detached, including
their original inline priority, pre-existing tracking attributes, root scale
property, and root scale attribute.
The LS2-3312 ancestor compensation is preserved to avoid double scaling inherited font sizes in newly inserted nested content. As in Sirius, explicit-size descendants inside an inserted scaled subtree receive the same compensation and may therefore be under-scaled.
A MutationObserver watches newly added light-DOM content only while scaling is
active. Insertions are batched so computed-style reads complete before inline
writes. One active controller is allowed per document. Each iframe document
requires its own controller, and shadow-root content is not traversed.
Only child-list changes are observed; existing elements that start or stop
matching because of class or attribute changes are not automatically updated.
destroy() is idempotent and terminal. After destruction, setScale() and
reset() throw, while scale and getScale() remain readable and return 1.
Selector configuration must be trusted and does not accept :has() or CSS
escapes. Each selector list accepts at most 100 entries, 2,048 characters per
selector, and 8,192 normalized characters in total.
The consumer owns selectors, exclusions, slider behavior, persistence, legacy zoom migration, feature flags, labels, announcements, and analytics. Layout reflow, clipping, and application style changes made while scaling is active remain consumer concerns.
Highlight focus
Highlight focus reproduces the Sirius focus indicator for links, buttons,
[role="button"], inputs, selects, and textareas when they match
:focus-visible.
import { createHighlightFocus } from '@leap/a11y-sdk/highlight-focus';
const highlightFocus = createHighlightFocus();
highlightFocus.enable();
highlightFocus.disable();
highlightFocus.destroy();Controllers begin disabled. enable() and disable() are idempotent, and a
disabled controller can be enabled again. destroy() is idempotent and
terminal; calling enable() afterward throws.
Read highlightFocus.isEnabled to check whether the indicator is currently
applied.
React usage
React consumers can align the controller lifecycle with a component effect:
import { useEffect } from 'react';
import { createHighlightFocus } from '@leap/a11y-sdk/highlight-focus';
export const HighlightFocus = (): null => {
useEffect(() => {
const highlightFocus = createHighlightFocus();
highlightFocus.enable();
return () => {
highlightFocus.destroy();
};
}, []);
return null;
};React is not a dependency of the SDK; this is a consumer integration example.
The indicator uses the Sirius visual treatment: a 3px solid #FFD700 outline
with a 2px offset. These values and selectors are intentionally fixed so the SDK
preserves existing behaviour. The gold outline may not provide 3:1 contrast
against light backgrounds.
declare const pageCspNonce: string;
const highlightFocus = createHighlightFocus({
root: document,
nonce: pageCspNonce,
});root accepts a Document or ShadowRoot, and each iframe requires its own
controller. A document-level controller cannot style inside shadow roots, and a
shadow-root controller cannot style nested shadow trees; create a controller
for each required root.
When a nonce is required, it must be generated unpredictably for each response
and match the nonce in that response's Content Security Policy header.
If the stylesheet is blocked, such as by a missing or mismatched nonce,
enable() throws.
Feature flags, widget state, preference persistence, announcements, and analytics remain the responsibility of the consuming application.
Font selection
Font selection provides the Sirius readable, atkinson, and
opendyslexic typography presets:
import { createFontSelection } from '@leap/a11y-sdk/font-selection';
const fontSelection = createFontSelection({
fontLoading: 'cdn',
targets: {
variableSelectors: [':root', '[class*="ag-theme-"]'],
fontFamilyVariables: ['--ag-font-family', '--bs-body-font-family'],
fontFamilySelectors: ['body', 'button', 'input', 'select', 'textarea'],
metricsSelectors: ['button', 'input', 'select', 'textarea'],
},
});
fontSelection.setFont('atkinson');
fontSelection.getFont();
fontSelection.setFont('none');
fontSelection.destroy();The fixed presets are:
readable: Verdana/Geneva,1.6line height,0.05emletter spacing.atkinson: Atkinson Hyperlegible with Segoe UI fallback,1.5line height,0.02emletter spacing.opendyslexic: OpenDyslexic with sans-serif fallback,1.6line height,0.05emletter spacing.
The SDK owns the fixed Sirius preset values and font asset loading. The consuming application supplies its CSS variables and selectors, keeping Sirius-specific component names outside the package.
Font assets load only when Atkinson Hyperlegible or OpenDyslexic is first
selected. Font loading defaults to none, so consumers must explicitly choose
cdn before the SDK contacts the existing Sirius sources. none and
readable make no external requests. Consumers can instead select local
loading with their own font paths. Local Atkinson assets must be same-origin
WOFF2 files, while local OpenDyslexic assets must be same-origin WOFF files.
Missing local paths fall back through the configured font-family stack.
Controllers start with none selected. Calling setFont('none') removes the
typography override while retaining loaded font assets. destroy() removes all
injected assets and permanently disables the controller. Only one active
controller is allowed per document.
Each iframe document requires its own controller. Shadow DOM content is affected only through inherited values or styles configured inside that shadow tree.
The optional nonce applies to injected <style> elements and the Google Fonts
stylesheet link. CDN mode also requires Content Security Policy allowances for
Google Fonts and jsDelivr. Local font URLs must be same-origin and require a
matching font-src policy.
Feature flags, widget state, persistence, labels, announcements, and analytics remain the responsibility of the consuming application.
Reading guide
Reading guide reproduces the Sirius mouse-following overlay:
import { createReadingGuide } from '@leap/a11y-sdk/reading-guide';
const readingGuide = createReadingGuide();
readingGuide.enable();
readingGuide.disable();
readingGuide.destroy();The guide has a fixed 40px CSS height plus 2px top and bottom borders. Sirius
positions it with a -20px transform offset using requestAnimationFrame. It
preserves the existing colours, z-index, and pointer-events: none behavior.
Tracking uses mousemove only; touch, pointer, and keyboard movement are not
handled.
Controllers begin disabled. enable() and disable() are idempotent while the
controller is active, destroy() is idempotent and terminal, and calling
enable() after destruction throws. isEnabled exposes the current state.
Creation injects the overlay and stylesheet immediately and allows one
active controller per document. The optional Document must already have
head, body, and defaultView; an optional CSP nonce can be supplied.
Creation throws if the stylesheet is blocked.
The host controller preserves the existing Sirius postMessage receiver and
accepts movement only from an iframe already present in that document. A
controller created inside an iframe renders a local guide but does not send
movement to its parent. This ticket does not add the missing iframe sender.
Consumers implementing a sender must post this message from the iframe window:
{
source: 'leap-accessibility-bridge',
version: 1,
event: 'readingGuideMove',
payload: { y: mouseEvent.clientY },
}Feature flags, widget controls, preference persistence, labels, announcements, analytics, and iframe sender integration remain the responsibility of the consuming application.
Development
yarn install
yarn typecheck
yarn buildBuild output is written to dist/ as ESM JavaScript, source maps, and TypeScript
declarations.
