Accessibility

Built-in focus management, ARIA helpers, and screen reader support.

Focus Management

What provides four tools for controlling focus: useFocus() for tracking/moving focus, useFocusRestore() for parent-controlled restore, useFocusTrap() for container-level trapping, and FocusTrap for declarative dialog boundaries.

useFocus()

Track the currently focused element and move focus programmatically:

import { useFocus } from 'what-framework';

const focus = useFocus();

// Read the currently focused element (reactive)
focus.current();  // HTMLElement or null

// Programmatically focus an element
focus.focus(myButtonRef.current);

// Blur the active element
focus.blur();

useFocusRestore()

Capture trigger focus before opening overlays, then restore it after close:

import { useFocusRestore } from 'what-framework';

const focusRestore = useFocusRestore();

function openDialog(e) {
  focusRestore.capture(e.currentTarget);
  setOpen(true);
}

function closeDialog() {
  setOpen(false);
  focusRestore.restore();
}

useFocusTrap(containerRef)

Trap focus within a container element. When active, pressing Tab at the last focusable element wraps to the first, and Shift+Tab at the first wraps to the last. Essential for modals, dialogs, and dropdown menus.

import { useFocusTrap } from 'what-framework';

function Modal({ onClose }) {
  const ref = { current: null };
  const trap = useFocusTrap(ref);

  // Activate the trap (focuses first focusable element)
  onMount(() => {
    const cleanup = trap.activate();
    return () => {
      cleanup?.();
      trap.deactivate();
    };
  });

  return (
    <div ref={ref} role="dialog" aria-modal="true">
      <h2>Confirm Action</h2>
      <p>Are you sure you want to proceed?</p>
      <button onClick={onClose}>Cancel</button>
      <button onClick={handleConfirm}>Confirm</button>
    </div>
  );
}

FocusTrap Component

Wrap the dialog subtree and conditionally mount it while open:

import { FocusTrap } from 'what-framework';

{isOpen() ? (
  <FocusTrap>
    <div class="dropdown-menu" role="dialog" aria-modal={true}>
      <a href="/profile">Profile</a>
      <a href="/settings">Settings</a>
      <button onClick={logout}>Log out</button>
    </div>
  </FocusTrap>
) : null}

Focus Restoration

For predictable UX, capture the trigger with useFocusRestore().capture(...) in parent logic and call restore() when closing the overlay.

Screen Reader Announcements

Announce dynamic content changes to screen readers using an ARIA live region managed by the framework.

import { announce, announceAssertive } from 'what-framework';

// Polite announcement (waits for screen reader to finish current speech)
announce('3 new items loaded', {
  priority: 'polite',    // 'polite' or 'assertive' (default: 'polite')
  timeout: 1000,         // auto-clear after ms (default: 1000)
});

// Assertive shortcut — interrupts current speech for urgent alerts
announceAssertive('Form submission failed. Please check your input.');

LiveRegion Component

For content that updates continuously, wrap it in a LiveRegion:

import { LiveRegion } from 'what-framework';

<LiveRegion priority="polite" atomic={true}>
  {() => `${itemCount()} items in cart`}
</LiveRegion>

ARIA Helpers

Hooks that manage ARIA attributes as reactive signals, keeping your markup in sync without manual bookkeeping.

useAriaExpanded

Manage expandable UI like accordions, menus, and collapsible panels. Returns reactive props for the trigger button and the content panel:

import { useAriaExpanded } from 'what-framework';

function Accordion({ title, children }) {
  const disclosure = useAriaExpanded(false);

  return (
    <div>
      <button {...disclosure.buttonProps()}>
        {title}
      </button>
      <div {...disclosure.panelProps()}>
        {children}
      </div>
    </div>
  );
}

The buttonProps() spread gives the button aria-expanded and an onClick toggle handler. The panelProps() spread sets hidden based on the expanded state. You can also control the state directly:

disclosure.expanded();  // reactive boolean
disclosure.toggle();    // flip open/closed
disclosure.open();      // force open
disclosure.close();     // force closed

useAriaSelected

Track selection state for tab lists, listboxes, and similar patterns:

import { useAriaSelected } from 'what-framework';

const tabs = useAriaSelected('home');

<div role="tablist">
  <button role="tab" {...tabs.itemProps('home')}>Home</button>
  <button role="tab" {...tabs.itemProps('about')}>About</button>
</div>

// Reactive state
tabs.selected();             // 'home'
tabs.isSelected('about');    // false
tabs.select('about');        // switch selection

useAriaChecked

Build accessible custom checkboxes with proper keyboard support (Space and Enter toggle):

import { useAriaChecked } from 'what-framework';

const checkbox = useAriaChecked(false);

<div {...checkbox.checkboxProps()}>
  {() => checkbox.checked() ? '[x]' : '[ ]'} Accept terms
</div>

// checkboxProps() provides: role, aria-checked, tabIndex, onClick, onKeyDown

Keyboard Navigation

The useRovingTabIndex hook implements the roving tabindex pattern for keyboard navigation in composite widgets like toolbars, menus, and listboxes. Only the focused item has tabIndex="0"; all others have tabIndex="-1".

import { useRovingTabIndex } from 'what-framework';

function Toolbar() {
  const items = ['Bold', 'Italic', 'Underline', 'Link'];
  const roving = useRovingTabIndex(items.length);

  return (
    <div role="toolbar" {...roving.containerProps()}>
      {items.map((label, i) =>
        <button {...roving.getItemProps(i)}>{label}</button>
      )}
    </div>
  );
}

Supported keys:

  • ArrowDown / ArrowRight — move to next item (wraps around)
  • ArrowUp / ArrowLeft — move to previous item (wraps around)
  • Home — jump to first item
  • End — jump to last item

You can also pass a signal or getter for dynamic item counts:

const itemCount = signal(5);
const roving = useRovingTabIndex(() => itemCount());

Help keyboard users bypass navigation and provide screen-reader-only content.

Renders a link that is hidden until focused. When activated, it moves focus to the target element:

import { SkipLink } from 'what-framework';

// Default: skips to #main
<SkipLink />

// Custom target and label
<SkipLink href="#content">Skip to main content</SkipLink>

VisuallyHidden

Hides content visually while keeping it accessible to screen readers. Uses the standard clip/overflow technique:

import { VisuallyHidden } from 'what-framework';

<button>
  <IconTrash />
  <VisuallyHidden>Delete item</VisuallyHidden>
</button>

// Renders as a span by default, use `as` for other elements
<VisuallyHidden as="div">Loading complete</VisuallyHidden>

ID Generation

Generate unique, stable IDs for connecting ARIA attributes across elements.

import { useId, useIds, useDescribedBy, useLabelledBy } from 'what-framework';

// Single unique ID
const id = useId('dialog');  // () => 'dialog-1'

// Multiple IDs at once
const [titleId, bodyId] = useIds(2, 'section');
// ['section-2', 'section-3']

useDescribedBy

Connect a description to an element via aria-describedby:

const desc = useDescribedBy('Password must be 8+ characters');

<input type="password" {...desc.describedByProps()} />
<desc.Description />
// Renders a hidden div with the description text, linked by ID

useLabelledBy

Connect a visible label to an element via aria-labelledby:

const label = useLabelledBy('Email Address');

<h3 {...label.labelProps()}>Email Address</h3>
<input type="email" {...label.labelledByProps()} />

Keyboard Helpers

Utility functions and constants for keyboard event handling.

import { Keys, onKey, onKeys } from 'what-framework';

// Keys constant — avoids string typos
Keys.Enter      // 'Enter'
Keys.Space      // ' '
Keys.Escape     // 'Escape'
Keys.ArrowUp    // 'ArrowUp'
Keys.ArrowDown  // 'ArrowDown'
Keys.Tab        // 'Tab'
Keys.Home       // 'Home'
Keys.End        // 'End'

// Handle a single key
<input onKeyDown={onKey(Keys.Escape, () => close())} />

// Handle multiple keys
<div onKeyDown={onKeys(
  [Keys.Enter, Keys.Space],
  (e) => { e.preventDefault(); activate(); }
)} />

Live Demo

An accessible accordion built with aria-expanded attributes. Try using your keyboard — each button correctly announces its expanded state to screen readers.

Live Demo — Accessible Accordion