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

useId parameters
PropTypeDefaultDescription
prefixstring-Optional prefix for the generated ID

Returns

string

Example

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

useIds parameters
PropTypeDefaultDescription
parts*readonly string[]-Array of part names to generate IDs for
prefixstring-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

useIdScope parameters
PropTypeDefaultDescription
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

useFocusTrap parameters
PropTypeDefaultDescription
activebooleantrueWhether the focus trap is active
initialFocusRefObject<HTMLElement>-Element to focus when trap activates
returnFocusbooleantrueReturn focus to previous element on deactivation
escapeDeactivatesbooleantrueWhether Escape key deactivates the trap
clickOutsideDeactivatesbooleanfalseWhether 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

useFocusTrapControls parameters
PropTypeDefaultDescription
activebooleanfalseWhether 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

useFocusManager parameters
PropTypeDefaultDescription
autoFocusbooleantrueFocus the element on mount
restoreFocusbooleanfalseRestore focus to the previous element on unmount
focusVisiblebooleanfalseShow 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

useFocusNeighbor parameters
PropTypeDefaultDescription
scopeRefObject<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

useKeyboard parameters
PropTypeDefaultDescription
handlers*Record<string, (e: KeyboardEvent) => void>-Map of key names to handler functions. Use 'Ctrl+A', 'Shift+Enter', 'Space' etc.
preventDefaultbooleantruePrevent default browser behaviour on matched keys
stopPropagationbooleantrueStop event propagation on matched keys
disabledbooleanfalseDisable 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

useMenuKeyboard parameters
PropTypeDefaultDescription
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

useTabsKeyboard parameters
PropTypeDefaultDescription
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

useGridKeyboard parameters
PropTypeDefaultDescription
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

useTypeAhead parameters
PropTypeDefaultDescription
items*string[]-Array of item labels to match against
onMatch*(match: string) => void-Called with the matched label
timeoutnumber500Milliseconds 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

useKeyPressed parameters
PropTypeDefaultDescription
targetKey*string-The key to track (e.g. 'Shift', 'Control')

Returns

boolean

Example

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

useAnnounceOnChange parameters
PropTypeDefaultDescription
value*T-Value to watch for changes
getMessage*(value: T) => string-Function that returns the announcement message
skipInitialbooleantrueSkip announcing on first render

Returns

void

Example

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

useAnnounceLoading parameters
PropTypeDefaultDescription
isLoading*boolean-Whether the loading state is active
loadingMessagestring'Loading...'Announced when loading starts
loadedMessagestring'Loaded'Announced when loading completes
errorMessagestring-Announced when an error is provided
errorunknown-Current error (triggers errorMessage when truthy)

Returns

void

Example

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

useRovingTabindex parameters
PropTypeDefaultDescription
itemCount*number-Number of items in the group
orientation'horizontal' | 'vertical''horizontal'Arrow key direction to use for navigation
wrapbooleantrueWhether 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

useRovingTabindexMap parameters
PropTypeDefaultDescription
ids*string[]-Ordered list of item IDs
orientation'horizontal' | 'vertical''vertical'Arrow key direction
wrapbooleantrueWhether 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>
  );
}