Hooks
React hooks for building custom accessible components. Import from @compa11y/react.
ID Generation
useId
Generate a unique, stable ID for ARIA attribute associations. The ID remains stable across renders.
Signature
useId(prefix?)Parameters
| Prop | Type | Default | Description |
|---|---|---|---|
prefix | string | - | Optional prefix for the generated ID |
Returns
stringExample
import { useId } from "@compa11y/react";
function LabeledInput({ label }) {
const id = useId("input");
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} />
</>
);
}useIds
Generate multiple related IDs for a component in a single call. All IDs share the same base and are guaranteed unique.
Signature
useIds(parts, prefix?)Parameters
| Prop | Type | Default | Description |
|---|---|---|---|
parts* | readonly string[] | - | Array of part names to generate IDs for |
prefix | string | - | Optional prefix for all generated IDs |
Returns
Record<string, string>Example
import { useIds } from "@compa11y/react";
function Combobox() {
const ids = useIds(["input", "listbox", "label"] as const, "combo");
// ids.input, ids.listbox, ids.label — all unique and related
return (
<>
<label id={ids.label}>Search</label>
<input
id={ids.input}
role="combobox"
aria-controls={ids.listbox}
aria-labelledby={ids.label}
/>
<ul id={ids.listbox} role="listbox" />
</>
);
}useIdScope
Create a scoped ID generator for complex components with many elements. All generated IDs share the same root.
Signature
useIdScope(componentName)Parameters
| Prop | Type | Default | Description |
|---|---|---|---|
componentName* | string | - | Name used as part of the ID prefix |
Returns
{ id: string; generate: (part: string) => string }Example
import { useIdScope } from "@compa11y/react";
function Dialog() {
const ids = useIdScope("dialog");
const titleId = ids.generate("title");
const descId = ids.generate("description");
return (
<div
id={ids.id}
role="dialog"
aria-labelledby={titleId}
aria-describedby={descId}
>
<h2 id={titleId}>Title</h2>
<p id={descId}>Description</p>
</div>
);
}Focus Management
useFocusTrap
Traps keyboard focus within a container element. Tab and Shift+Tab cycle between focusable elements inside the container only. Used internally by Dialog.
Signature
useFocusTrap<T>(options?)Parameters
| Prop | Type | Default | Description |
|---|---|---|---|
active | boolean | true | Whether the focus trap is active |
initialFocus | RefObject<HTMLElement> | - | Element to focus when trap activates |
returnFocus | boolean | true | Return focus to previous element on deactivation |
escapeDeactivates | boolean | true | Whether Escape key deactivates the trap |
clickOutsideDeactivates | boolean | false | Whether clicking outside deactivates the trap |
onDeactivate | () => void | - | Callback fired when the trap is deactivated |
Returns
RefObject<T>Example
import { useFocusTrap } from "@compa11y/react";
function Modal({ isOpen, onClose }) {
const trapRef = useFocusTrap<HTMLDivElement>({
active: isOpen,
onDeactivate: onClose,
escapeDeactivates: true,
returnFocus: true,
});
return (
<div ref={trapRef} role="dialog" aria-modal="true">
<button onClick={onClose}>Close</button>
<p>Trapped content</p>
</div>
);
}useFocusTrapControls
Imperative control over a focus trap. Lets you activate, deactivate, pause, and unpause the trap programmatically.
Signature
useFocusTrapControls(options?)Parameters
| Prop | Type | Default | Description |
|---|---|---|---|
active | boolean | false | Whether the trap starts active |
Returns
{ ref: RefObject<HTMLElement>; activate: () => void; deactivate: () => void; pause: () => void; unpause: () => void; isActive: boolean }Example
import { useFocusTrapControls } from "@compa11y/react";
function Drawer({ open }) {
const { ref, activate, deactivate, isActive } = useFocusTrapControls();
useEffect(() => {
if (open) activate();
else deactivate();
}, [open]);
return <div ref={ref}>{/* drawer content */}</div>;
}useFocusVisible
Detects whether focus was triggered by keyboard navigation (Tab) or mouse/touch. Returns true only for keyboard-initiated focus, enabling keyboard-only focus rings.
Signature
useFocusVisible()Returns
{ isFocusVisible: boolean; focusProps: { onFocus: ...; onBlur: ... } }Example
import { useFocusVisible } from "@compa11y/react";
function CustomButton({ children }) {
const { isFocusVisible, focusProps } = useFocusVisible();
return (
<button
{...focusProps}
className={isFocusVisible ? "focus-ring" : ""}
>
{children}
</button>
);
}useFocusManager
Automatically focus an element on mount and optionally restore focus on unmount. Useful for panels, drawers, and inline editors.
Signature
useFocusManager(options?)Parameters
| Prop | Type | Default | Description |
|---|---|---|---|
autoFocus | boolean | true | Focus the element on mount |
restoreFocus | boolean | false | Restore focus to the previous element on unmount |
focusVisible | boolean | false | Show focus ring when programmatically focused |
Returns
RefObject<HTMLElement>Example
import { useFocusManager } from "@compa11y/react";
function InlineEditor() {
const ref = useFocusManager({ autoFocus: true, restoreFocus: true });
return <input ref={ref} />;
}useFocusControl
Imperative focus control — call focus() programmatically with an optional visible focus ring.
Signature
useFocusControl<T>()Returns
{ ref: RefObject<T>; focus: (options?: { visible?: boolean }) => void }Example
import { useFocusControl } from "@compa11y/react";
function Notification({ message }) {
const { ref, focus } = useFocusControl<HTMLButtonElement>();
useEffect(() => {
focus({ visible: true }); // Focus with keyboard-style ring
}, []);
return <button ref={ref}>{message}</button>;
}useFocusWithin
Track whether an element or any of its descendants currently have focus. Useful for styling active composite widgets.
Signature
useFocusWithin<T>()Returns
{ ref: RefObject<T>; hasFocus: boolean; focusWithinProps: { onFocus: ...; onBlur: ... } }Example
import { useFocusWithin } from "@compa11y/react";
function SearchGroup() {
const { ref, hasFocus, focusWithinProps } = useFocusWithin<HTMLDivElement>();
return (
<div
ref={ref}
{...focusWithinProps}
className={hasFocus ? "active-group" : ""}
>
<input />
<button>Search</button>
</div>
);
}useFocusNeighbor
Find and focus the nearest focusable sibling. Useful when the current element is about to be removed or disabled — moves focus gracefully to an adjacent item.
Signature
useFocusNeighbor<T>(options?)Parameters
| Prop | Type | Default | Description |
|---|---|---|---|
scope | RefObject<HTMLElement> | - | Container to search within (defaults to parent element) |
prefer | 'previous' | 'next' | 'previous' | Which sibling direction to try first |
Returns
{ ref: RefObject<T>; focusNeighbor: () => void }Example
import { useFocusNeighbor } from "@compa11y/react";
function RemovableItem({ item, onRemove }) {
const { ref, focusNeighbor } = useFocusNeighbor<HTMLLIElement>();
const handleRemove = () => {
focusNeighbor(); // Move focus before removal
onRemove(item.id);
};
return (
<li ref={ref}>
{item.label}
<button onClick={handleRemove}>Remove</button>
</li>
);
}useFocusReturn
Save the current focus target and restore it later. If the saved element is disabled or removed, focus moves to its nearest neighbor automatically.
Signature
useFocusReturn()Returns
{ save: (element?: HTMLElement) => void; returnFocus: (options?: { prefer?: 'previous' | 'next'; fallback?: HTMLElement }) => void; clear: () => void }Example
import { useFocusReturn } from "@compa11y/react";
function ModalFlow() {
const { save, returnFocus } = useFocusReturn();
const openModal = () => {
save(); // Remember current focus (e.g., the trigger button)
showModal();
};
const closeModal = () => {
hideModal();
returnFocus(); // Go back to trigger — or its neighbor if disabled
};
}Keyboard
useKeyboard
Generic keyboard event handler that normalizes keys across browsers. Use 'Space' (not ' ') for the space key — the library normalizes event.key internally.
Signature
useKeyboard(handlers, options?)Parameters
| Prop | Type | Default | Description |
|---|---|---|---|
handlers* | Record<string, (e: KeyboardEvent) => void> | - | Map of key names to handler functions. Use 'Ctrl+A', 'Shift+Enter', 'Space' etc. |
preventDefault | boolean | true | Prevent default browser behaviour on matched keys |
stopPropagation | boolean | true | Stop event propagation on matched keys |
disabled | boolean | false | Disable all handlers |
Returns
{ onKeyDown: React.KeyboardEventHandler }Example
import { useKeyboard } from "@compa11y/react";
function Listbox() {
const { onKeyDown } = useKeyboard({
ArrowDown: () => focusNext(),
ArrowUp: () => focusPrevious(),
Enter: () => selectItem(),
Escape: () => close(),
"Ctrl+A": () => selectAll(),
Space: () => toggleItem(), // Use 'Space' not ' '
});
return <ul onKeyDown={onKeyDown} role="listbox">{/* ... */}</ul>;
}useMenuKeyboard
Pre-built keyboard pattern for menus and action menus. Handles Down/Up/Home/End/Enter/Escape.
Signature
useMenuKeyboard(options)Parameters
| Prop | Type | Default | Description |
|---|---|---|---|
onDown | () => void | - | Arrow Down pressed |
onUp | () => void | - | Arrow Up pressed |
onEnter | () => void | - | Enter or Space pressed |
onEscape | () => void | - | Escape pressed |
onHome | () => void | - | Home pressed |
onEnd | () => void | - | End pressed |
Returns
{ onKeyDown: React.KeyboardEventHandler }Example
import { useMenuKeyboard } from "@compa11y/react";
function Menu({ items }) {
const { onKeyDown } = useMenuKeyboard({
onDown: () => focusNext(),
onUp: () => focusPrevious(),
onEnter: () => selectItem(),
onEscape: () => closeMenu(),
onHome: () => focusFirst(),
onEnd: () => focusLast(),
});
return <ul role="menu" onKeyDown={onKeyDown}>{/* ... */}</ul>;
}useTabsKeyboard
Pre-built keyboard pattern for tab navigation. Handles Left/Right/Home/End for horizontal tabs.
Signature
useTabsKeyboard(options)Parameters
| Prop | Type | Default | Description |
|---|---|---|---|
onLeft | () => void | - | Arrow Left pressed |
onRight | () => void | - | Arrow Right pressed |
onHome | () => void | - | Home pressed |
onEnd | () => void | - | End pressed |
Returns
{ onKeyDown: React.KeyboardEventHandler }Example
import { useTabsKeyboard } from "@compa11y/react";
function TabList() {
const { onKeyDown } = useTabsKeyboard({
onLeft: () => previousTab(),
onRight: () => nextTab(),
onHome: () => firstTab(),
onEnd: () => lastTab(),
});
return <div role="tablist" onKeyDown={onKeyDown}>{/* ... */}</div>;
}useGridKeyboard
Pre-built keyboard pattern for 2D grid navigation. Handles all four arrow directions plus Ctrl+Home/End.
Signature
useGridKeyboard(options)Parameters
| Prop | Type | Default | Description |
|---|---|---|---|
onUp | () => void | - | Arrow Up pressed |
onDown | () => void | - | Arrow Down pressed |
onLeft | () => void | - | Arrow Left pressed |
onRight | () => void | - | Arrow Right pressed |
onCtrlHome | () => void | - | Ctrl+Home pressed |
onCtrlEnd | () => void | - | Ctrl+End pressed |
Returns
{ onKeyDown: React.KeyboardEventHandler }Example
import { useGridKeyboard } from "@compa11y/react";
function DataGrid() {
const { onKeyDown } = useGridKeyboard({
onUp: () => moveUp(),
onDown: () => moveDown(),
onLeft: () => moveLeft(),
onRight: () => moveRight(),
onCtrlHome: () => goToFirstCell(),
onCtrlEnd: () => goToLastCell(),
});
return <div role="grid" onKeyDown={onKeyDown}>{/* ... */}</div>;
}useTypeAhead
Type-ahead search for lists — pressing character keys jumps to matching items. Resets after a configurable timeout.
Signature
useTypeAhead(items, onMatch, options?)Parameters
| Prop | Type | Default | Description |
|---|---|---|---|
items* | string[] | - | Array of item labels to match against |
onMatch* | (match: string) => void | - | Called with the matched label |
timeout | number | 500 | Milliseconds before the search string resets |
Returns
{ onKeyDown: React.KeyboardEventHandler; reset: () => void }Example
import { useTypeAhead } from "@compa11y/react";
function FruitList({ items }) {
const { onKeyDown, reset } = useTypeAhead(
items.map((i) => i.label),
(match) => {
const index = items.findIndex((i) => i.label === match);
setFocusedIndex(index);
},
{ timeout: 500 }
);
return (
<ul role="listbox" onKeyDown={onKeyDown} onBlur={reset}>
{/* ... */}
</ul>
);
}useKeyPressed
Track whether a specific key is currently held down. Useful for implementing multi-select with Shift held.
Signature
useKeyPressed(targetKey)Parameters
| Prop | Type | Default | Description |
|---|---|---|---|
targetKey* | string | - | The key to track (e.g. 'Shift', 'Control') |
Returns
booleanExample
import { useKeyPressed } from "@compa11y/react";
function SelectableList() {
const isShiftHeld = useKeyPressed("Shift");
const handleSelect = (index) => {
if (isShiftHeld) {
extendSelection(index); // Shift+Click = range select
} else {
setSingleSelection(index);
}
};
}Screen Reader Announcements
useAnnouncer
Access the screen reader announcement system. Supports polite (non-interrupting) and assertive (interrupting) announcements, plus debounced queuing.
Signature
useAnnouncer()Returns
{ announce: (msg: string, options?) => void; polite: (msg: string) => void; assertive: (msg: string) => void; queue: (msg: string, options?) => void; clear: () => void }Example
import { useAnnouncer } from "@compa11y/react";
function SaveButton() {
const { polite, assertive } = useAnnouncer();
const handleSave = async () => {
try {
await save();
polite("Changes saved successfully"); // Won't interrupt
} catch {
assertive("Error: failed to save"); // Interrupts current speech
}
};
return <button onClick={handleSave}>Save</button>;
}useAnnounceOnChange
Automatically announce a screen reader message whenever a value changes. Optionally skips the initial render.
Signature
useAnnounceOnChange(value, getMessage, options?)Parameters
| Prop | Type | Default | Description |
|---|---|---|---|
value* | T | - | Value to watch for changes |
getMessage* | (value: T) => string | - | Function that returns the announcement message |
skipInitial | boolean | true | Skip announcing on first render |
Returns
voidExample
import { useAnnounceOnChange } from "@compa11y/react";
function SelectionCounter({ selectedCount }) {
useAnnounceOnChange(
selectedCount,
(count) => `${count} item${count === 1 ? "" : "s"} selected`,
{ skipInitial: true }
);
return <span>{selectedCount} selected</span>;
}useAnnounceLoading
Announce loading state transitions — when loading starts, finishes, or errors. Handles the polite/assertive distinction automatically.
Signature
useAnnounceLoading(isLoading, options?)Parameters
| Prop | Type | Default | Description |
|---|---|---|---|
isLoading* | boolean | - | Whether the loading state is active |
loadingMessage | string | 'Loading...' | Announced when loading starts |
loadedMessage | string | 'Loaded' | Announced when loading completes |
errorMessage | string | - | Announced when an error is provided |
error | unknown | - | Current error (triggers errorMessage when truthy) |
Returns
voidExample
import { useAnnounceLoading } from "@compa11y/react";
function DataTable({ isLoading, error }) {
useAnnounceLoading(isLoading, {
loadingMessage: "Loading data...",
loadedMessage: "Data loaded successfully",
errorMessage: "Failed to load data",
error,
});
if (isLoading) return <p>Loading...</p>;
return <table>{/* ... */}</table>;
}Roving Tabindex
useRovingTabindex
Implement the roving tabindex pattern — only one item in a group has tabindex='0' at a time; arrow keys move between items. Used internally by RadioGroup and Tabs.
Signature
useRovingTabindex(options)Parameters
| Prop | Type | Default | Description |
|---|---|---|---|
itemCount* | number | - | Number of items in the group |
orientation | 'horizontal' | 'vertical' | 'horizontal' | Arrow key direction to use for navigation |
wrap | boolean | true | Whether navigation wraps from last to first |
Returns
{ activeIndex: number; getItemProps: (index: number) => { tabIndex: number; onKeyDown: ...; onFocus: ... } }Example
import { useRovingTabindex } from "@compa11y/react";
function Toolbar() {
const { activeIndex, getItemProps } = useRovingTabindex({
itemCount: 3,
orientation: "horizontal",
wrap: true,
});
return (
<div role="toolbar">
<button {...getItemProps(0)}>Cut</button>
<button {...getItemProps(1)}>Copy</button>
<button {...getItemProps(2)}>Paste</button>
</div>
);
}useRovingTabindexMap
Same as useRovingTabindex but items are indexed by string IDs rather than numeric indices. Useful when items can be added or removed dynamically.
Signature
useRovingTabindexMap(ids, options?)Parameters
| Prop | Type | Default | Description |
|---|---|---|---|
ids* | string[] | - | Ordered list of item IDs |
orientation | 'horizontal' | 'vertical' | 'vertical' | Arrow key direction |
wrap | boolean | true | Whether navigation wraps |
Returns
{ activeId: string | null; getItemProps: (id: string) => { tabIndex: number; onKeyDown: ...; onFocus: ... } }Example
import { useRovingTabindexMap } from "@compa11y/react";
function Menu({ items }) {
const ids = items.map((i) => i.id);
const { activeId, getItemProps } = useRovingTabindexMap(ids, {
orientation: "vertical",
});
return (
<ul role="menu">
{items.map((item) => (
<li key={item.id} role="menuitem" {...getItemProps(item.id)}>
{item.label}
</li>
))}
</ul>
);
}