The usual accessibility workflow is: run an automated audit, fix whatever’s flagged red, ship it. The problem is that automated tools, axe, Lighthouse, whatever’s wired into CI, can only catch what’s mechanically detectable from markup: missing alt text, insufficient contrast ratios, a button with no accessible name. They structurally cannot catch whether the page actually works for someone using a keyboard or a screen reader, because that’s a behavior question, not a markup question. A clean audit and an unusable page are both entirely possible at the same time.

Unplug your mouse first

This is the fastest real test that exists and most teams never run it. Put the mouse away and try to use the page with Tab, Shift+Tab, Enter, and arrow keys alone. Three things usually surface within a minute: something you can see but can’t reach (a custom dropdown, a card with an onClick but no keyboard handler), a focus order that jumps around the screen in a way that doesn’t match the visual layout, and a focus outline that’s been styled away entirely, leaving no visible indicator of where you are on the page at all.

Focus management inside a modal or dialog

This is the single most common real-world accessibility bug in custom UI, and it’s entirely a behavior problem, not a markup one. The correct sequence: when the dialog opens, move focus into it; while it’s open, Tab and Shift+Tab should cycle only through elements inside it, never escaping to the page behind it; when it closes, focus should return to whatever element opened it, not reset to the top of the page.

function Dialog({ open, onClose, children }) {
  const dialogRef = useRef(null);
  const lastFocusedRef = useRef(null);

  useEffect(() => {
    if (open) {
      lastFocusedRef.current = document.activeElement;
      dialogRef.current?.focus();
    } else {
      lastFocusedRef.current?.focus();
    }
  }, [open]);

  function handleKeyDown(e) {
    if (e.key === "Escape") onClose();
    if (e.key === "Tab") {
      const focusable = dialogRef.current.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      const first = focusable[0];
      const last = focusable[focusable.length - 1];
      if (e.shiftKey && document.activeElement === first) {
        e.preventDefault();
        last.focus();
      } else if (!e.shiftKey && document.activeElement === last) {
        e.preventDefault();
        first.focus();
      }
    }
  }

  if (!open) return null;
  return (
    <div role="dialog" aria-modal="true" ref={dialogRef} tabIndex={-1} onKeyDown={handleKeyDown}>
      {children}
    </div>
  );
}

No automated tool checks whether Tab actually loops correctly inside this. The only way to know is to open the dialog and press Tab until you’ve gone all the way around.

Contrast in both themes, and in every state

A contrast checker run once, on the default state, in one theme, misses most of the real surface area. If the product supports dark mode, every token needs checking in both themes separately, a pairing that passes in light mode can easily fail in dark mode with the same relative colors. State colors are the ones that get skipped entirely: placeholder text, disabled buttons, a focus ring against a colored background, error text at whatever opacity someone picked without checking. These are exactly the states a design system review tends to skip because they’re not the “happy path” someone is looking at.

Five minutes with an actual screen reader

You don’t need screen reader expertise for this to be worth doing. On a Mac, Cmd+F5 turns on VoiceOver; on Windows, NVDA is free. Load the page and just listen. This catches things no automated tool can: a button whose only content is an icon, announced as “button” with no indication of what it does; a live region (a toast, a form error) that appears visually but is never announced because it’s missing aria-live; a heading structure that skips levels in a way that makes the page impossible to navigate by heading, which is how screen reader users skim a page the way a sighted user skims with their eyes.

The gap this leaves, and why it’s worth closing

None of this replaces automated tooling, it still catches real issues fast and belongs in CI. The gap is that a clean automated report gets treated as “accessibility done,” when it’s closer to “the easy third is done.” The keyboard-only pass and the five-minute screen reader check together take less time than writing this sentence took to read twice, and they catch the failures that actually stop someone from using the product, not just the ones a linter can see in the markup.