a11y-scroll-cue
v1.0.0
Published
A progressive-enhancement cue for horizontally overflowing content.
Maintainers
Readme
A11y Scroll Cue
A small, dependency-free progressive enhancement that makes horizontal overflow easier to discover. It shows a visible text cue, dismisses it after real horizontal scrolling, and adds a keyboard tab stop only while the scrollport overflows.
The plugin works with tables, code blocks, card strips, and other semantic content. It is framework-agnostic, ESM-only, and does not initialize on import.
Installation
npm install a11y-scroll-cue
pnpm add a11y-scroll-cue
yarn add a11y-scroll-cueUsage
import { createA11yScrollCue } from "a11y-scroll-cue";
import "a11y-scroll-cue/styles.css";
const root = document.querySelector("[data-a11y-scroll-cue]");
if (root instanceof HTMLElement) {
const cue = createA11yScrollCue(root, {
regionLabel: "Feature comparison table",
});
// cue.update();
// cue.reset();
// cue.dismiss();
// cue.destroy();
}To initialize every matching root:
import { initA11yScrollCueAll } from "a11y-scroll-cue";
const instances = initA11yScrollCueAll();CSS
Import a11y-scroll-cue/styles.css for the default cue, focus indicator, and
state styles. The component exposes these custom properties:
--a11y-scroll-cue-background--a11y-scroll-cue-color--a11y-scroll-cue-border-color--a11y-scroll-cue-hint-background--a11y-scroll-cue-hint-color--a11y-scroll-cue-shadow--a11y-scroll-cue-radius--a11y-scroll-cue-z-index
The default transition respects prefers-reduced-motion.
HTML structure
Use a root and an explicit child scrollport. Keeping the generated cue outside the scrollport prevents it from becoming part of the scrollable content.
<div
class="a11y-scroll-cue"
data-a11y-scroll-cue
data-a11y-scroll-cue-message="Scroll horizontally to see all columns"
data-a11y-scroll-cue-region-label="Feature comparison table"
>
<div data-a11y-scroll-cue-scrollport>
<table>
<!-- Wide semantic table content -->
</table>
</div>
</div>Without JavaScript, the original content remains available and horizontally scrollable when the page's own layout styles provide overflow.
Options
| Option | Data attribute | Default |
| --- | --- | --- |
| message | data-a11y-scroll-cue-message | "Scroll horizontally" |
| threshold | data-a11y-scroll-cue-threshold | 2 |
| hideOnScroll | data-a11y-scroll-cue-hide-on-scroll | true |
| focusable | data-a11y-scroll-cue-focusable | true |
| observeMutations | data-a11y-scroll-cue-observe-mutations | true |
| regionLabel | data-a11y-scroll-cue-region-label | "" |
| describeScrollport | data-a11y-scroll-cue-describe-scrollport | false |
Invalid dataset booleans and negative or non-numeric thresholds fall back to the defaults.
API
createA11yScrollCue(root, options?)enhances one root and returns its existing instance when called repeatedly for the same element.initA11yScrollCueAll(options?, scope?)enhances matching roots in a document or subtree.A11yScrollCueis the plugin-specific class.update()measures overflow and synchronizes visual and semantic state.reset()makes an eligible dismissed cue visible again.dismiss(reason?)hides the cue until reset or until overflow disappears.destroy()removes listeners, observers, generated elements, classes, and plugin-added attributes.
The package also exports frozen DEFAULT_OPTIONS, SELECTORS, CLASSES,
ATTRIBUTES, and EVENTS constants.
Events
Lifecycle events dispatch synchronously from the component root. Every event
uses bubbles: true, composed: false, and cancelable: false. Every detail
includes instance and root.
| Event | Trigger | Additional detail |
| --- | --- | --- |
| a11y-scroll-cue:init | Instance and observers installed | hasOverflow |
| a11y-scroll-cue:update | State reconciliation completed | hasOverflow, visible, dismissed |
| a11y-scroll-cue:ready | Initial state applied | hasOverflow, visible, dismissed |
| a11y-scroll-cue:error | Required scrollport missing | code, message |
| a11y-scroll-cue:overflow-change | Overflow changes after initialization | hasOverflow, previousHasOverflow |
| a11y-scroll-cue:dismiss | Cue enters the dismissed state | reason, hasOverflow, visible, dismissed |
| a11y-scroll-cue:reset | Reset and its update complete | changed, hasOverflow, visible, dismissed |
| a11y-scroll-cue:destroy | Cleanup completes | finalState |
Successful initialization has this guaranteed order:
init → update → readyThe initial measurement establishes the baseline and does not emit
overflow-change. Later overflow changes emit overflow-change immediately
before update. A reset emits update before reset. destroy is terminal:
no later event is emitted, including when a listener destroys the instance
during an active event sequence.
Because initialization is synchronous, attach direct or delegated listeners
before calling createA11yScrollCue():
import {
EVENTS,
createA11yScrollCue,
type A11yScrollCueEvent,
} from "a11y-scroll-cue";
const root = document.querySelector("[data-a11y-scroll-cue]");
if (root instanceof HTMLElement) {
root.addEventListener(EVENTS.ready, (event) => {
const readyEvent = event as A11yScrollCueEvent<typeof EVENTS.ready>;
console.log(readyEvent.detail.hasOverflow);
});
createA11yScrollCue(root);
}These events are observations, not commands, and cannot cancel plugin
behavior. Bubbling events do not cross a Shadow DOM boundary because they are
not composed. initA11yScrollCueAll() initializes roots in DOM order and
finishes each root's synchronous sequence before starting the next; it does
not emit an aggregate event.
Accessibility notes
- The cue uses visible text, a border, and an arrow instead of color alone.
- The cue is decorative to assistive technology by default; no live region is created.
- A scrollport receives
tabindex="0"only while horizontal overflow exists andfocusableis enabled. - When
regionLabelis provided, an overflowing focusable scrollport receivesrole="region"and an accessible name unless it already has one. describeScrollportopt-in links the cue text witharia-describedby.- Keyboard scrolling uses the browser's native behavior. The plugin does not override arrow, Page Up, Page Down, Home, or End keys.
destroy()restores plugin-added focus and region attributes. Existing author-provided attributes are preserved.
The cue improves overflow discoverability, but it cannot guarantee how every browser, assistive technology, or input method exposes scrolling.
Examples
- Basic examples compare the cue across a wide table, code sample, and card strip.
- Accessibility audit queue demonstrates a real-world release workflow with dismissed, reset, status, and opt-in description states.
After installing dependencies, run npm run pages:build and serve docs/ with
any local static server.
Docs metadata
Documentation aggregators can import structured metadata without parsing this README:
import { docs } from "a11y-scroll-cue/docs";GitHub Pages
npm run pages:build rebuilds the package and deterministically regenerates
the committed docs/ directory. Do not edit docs/ by hand.
For one-time hosting setup, open repository Settings → Pages, choose Deploy from a branch, then select main and /docs. The expected URL is https://vmitsaras.github.io/A11y-Scroll-Cue/.
Development
npm install
npm run build
npm run typecheck
npm test
npm run pack:checkLicense
MIT © 2026 Vasileios Mitsaras.
