A lightweight utility that wires global arrow-key listeners to move focus
between tabindex-managed DOM elements based on their visual positions.
- Visual-position navigation (not DOM order)
tabindex-based focusable detection with custom filters- Global keydown handling plus user-provided side listeners
- Framework-agnostic TypeScript build (ESM + CJS) with generated types
- Ready-to-use Vitest + jsdom environment for development
npm install focus-arrowimport { FocusArrow } from 'focus-arrow';
const manager = new FocusArrow({
scope: document.querySelector('[data-focus-scope]') as HTMLElement,
onNavigate: ({ from, to, direction }) => {
console.info(`${from?.id ?? 'nothing'} -> ${to?.id ?? 'nothing'} via ${direction}`);
}
});
// Add extra listeners if needed
const removeListener = manager.addKeydownListener((event) => {
if (event.key === 'ArrowRight') {
console.debug('Custom hook!', event);
}
});
// Later…
removeListener();
manager.destroy();| Option | Type | Default | Description |
|---|---|---|---|
scope |
Document | HTMLElement |
document |
Element tree used to look up focusable nodes |
eventTarget |
Document | HTMLElement |
document |
Target that listens for keydown events |
shouldWrap |
boolean |
true |
Wrap to the first/last element if needed |
filter |
(el: HTMLElement) => boolean |
— | Custom predicate to include/exclude nodes |
onNavigate |
({ from, to, direction, event }) => void |
— | Callback fired after every navigation try |
Focusable elements are detected strictly through the tabIndex >= 0 rule.
| Script | Purpose |
|---|---|
npm run dev |
Watch mode for Vitest (ideal while iterating) |
npm run test |
Single-run Vitest suite using jsdom |
npm run typecheck |
TypeScript type-checking without emit |
npm run build |
Bundle with tsup (outputs ESM, CJS, and d.ts) |
Vitest is already configured with a jsdom environment. See
tests/focusArrow.test.ts for examples that stub getBoundingClientRect to
simulate different visual layouts.