Accessibility Hooks

Last updated on 2026-04-14

The A11y Starter Kit includes 5 custom React hooks that handle common accessibility patterns. All hooks are standalone and can be copied into any React project.

useFocusTrap

Traps keyboard focus within a container element (modals, dialogs, drawers).

File: hooks/use-focus-trap.ts

import { useFocusTrap } from '@/hooks/use-focus-trap';

function Modal({ isOpen, onClose }) {
    const ref = useFocusTrap(isOpen);

    if (!isOpen) return null;

    return (
        <div ref={ref} role="dialog" aria-modal="true">
            <h2>Modal Title</h2>
            <p>Modal content here.</p>
            <button onClick={onClose}>Close</button>
        </div>
    );
}

Behavior:

  • On open: focuses the first focusable element inside the container
  • Tab/Shift+Tab wraps within the container boundaries
  • Escape key closes (if onClose is wired up)
  • On close: returns focus to the element that triggered the modal

useKeyboardNavigation

Enables arrow key navigation for lists, menus, and grids.

File: hooks/use-keyboard-navigation.ts

import { useKeyboardNavigation } from '@/hooks/use-keyboard-navigation';

function Menu({ items }) {
    const { activeIndex, containerRef } = useKeyboardNavigation({
        itemCount: items.length,
        orientation: 'vertical', // or 'horizontal'
    });

    return (
        <ul ref={containerRef} role="menu">
            {items.map((item, i) => (
                <li key={item.id} role="menuitem" tabIndex={i === activeIndex ? 0 : -1}>
                    {item.label}
                </li>
            ))}
        </ul>
    );
}

Behavior:

  • Arrow Up/Down (vertical) or Left/Right (horizontal) move between items
  • Home/End jump to first/last item
  • Only the active item is in the tab order (tabIndex={0})
  • Wraps from last to first and vice versa

useAnnounce

Provides a function to announce messages to screen readers via ARIA live regions.

File: hooks/use-announce.ts

import { useAnnounce } from '@/hooks/use-announce';

function SaveButton() {
    const announce = useAnnounce();

    const handleSave = async () => {
        await saveData();
        announce('Changes saved successfully');
    };

    return <button onClick={handleSave}>Save</button>;
}

Parameters:

  • message (string) -- the text to announce
  • politeness ('polite' | 'assertive') -- defaults to 'polite'

Use 'assertive' for errors and urgent messages. Use 'polite' for confirmations and status updates.

Requires: The <LiveRegion /> component must be rendered in the layout (already included in the root layout).

useReducedMotion

Detects the user's prefers-reduced-motion setting.

File: hooks/use-reduced-motion.ts

import { useReducedMotion } from '@/hooks/use-reduced-motion';

function AnimatedCard() {
    const prefersReduced = useReducedMotion();

    return (
        <div className={prefersReduced ? '' : 'animate-fade-in'}>
            Card content
        </div>
    );
}

Returns: true if the user prefers reduced motion, false otherwise.

Use this to:

  • Disable CSS animations and transitions
  • Skip auto-playing carousels
  • Reduce parallax and scroll effects

useMobile

Detects whether the viewport is below a mobile breakpoint.

File: hooks/use-mobile.ts

import { useMobile } from '@/hooks/use-mobile';

function ResponsiveNav() {
    const isMobile = useMobile();

    return isMobile ? <MobileMenu /> : <DesktopNav />;
}

Returns: true if the viewport width is below the mobile breakpoint (768px by default).

Using Hooks in Your Own Project

All hooks are standalone files with no external dependencies beyond React. To use them in your project:

  1. Copy the hook file from hooks/ into your project
  2. Update the import path
  3. For useAnnounce, also copy components/a11y/live-region.tsx and render it in your root layout