# Modal dialogs trap focus, restore focus when closed, and use role="dialog" with aria-label

- **Pattern:** `ab-001608` (`gov-section-508.forms-interactive.modal-focus`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001608
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001608)

## Why it matters

Modal dialogs that do not trap keyboard focus allow Tab to move focus outside the modal into background content that is visually obscured, leaving keyboard users unable to see or interact with the focused element. Modals without `role="dialog"` are not announced as dialogs by screen readers, so users receive no cue that a blocking overlay has appeared. When focus fails to return to the trigger element on close, users lose their place in the page entirely. WCAG 2.2 SC 2.1.1 (Level A) and SC 4.1.2 (Level A) both apply; Section 508 2018 Refresh 502.3.2 requires that the full dialog lifecycle be accessible to AT. Broken modal focus is one of the most common Section 508 findings in government application audits.

## Severity rationale

Medium because broken modal focus management disrupts keyboard and screen reader users at a critical interaction point, but it does not prevent access to content outside the modal.

## Remediation

Implement focus trapping and restoration in `src/components/Modal.tsx`. Store a ref to the trigger element before opening, move focus to the first focusable element on open, and return focus on close:
```tsx
const triggerRef = useRef<HTMLButtonElement>(null);
const firstFocusRef = useRef<HTMLButtonElement>(null);

const openModal = () => {
  setIsOpen(true);
  // Move focus after render
  requestAnimationFrame(() => firstFocusRef.current?.focus());
};

const closeModal = () => {
  setIsOpen(false);
  triggerRef.current?.focus(); // Restore to trigger
};

{isOpen && (
  <div
    role="dialog"
    aria-modal="true"
    aria-labelledby="modal-title"
  >
    <h2 id="modal-title">Confirm deletion</h2>
    <button ref={firstFocusRef} onClick={closeModal}>Cancel</button>
    <button onClick={handleDelete}>Delete</button>
  </div>
)}
```
Use a library like `focus-trap-react` for robust cross-browser focus trapping rather than hand-rolling the Tab key handler.

## Detection

- **ID:** `modal-focus`
- **Severity:** `medium`
- **What to look for:** Count all relevant instances and enumerate each. Examine all modal or dialog elements. Check whether they have `role="dialog"` and an `aria-label` describing the dialog. Verify that Tab key navigation is trapped within the dialog (last focusable element tabs back to first). Verify that focus is restored to the triggering element when the dialog is closed.
- **Pass criteria:** All modals have `role="dialog"` and `aria-label`. At least 1 implementation must be verified. Focus is trapped within the modal while open (Tab loops within the modal). When closed, focus returns to the button or element that opened the modal.
- **Fail criteria:** Modals lack `role="dialog"` or `aria-label`, or focus is not trapped, or focus is not restored on close.
- **Skip (N/A) when:** No modal dialogs exist.
- **Detail on fail:** Example: `"Confirmation modal in the delete workflow has no role='dialog' or aria-label. When Tab is pressed on the last button, focus moves to the page behind the modal instead of looping back to the first button. When the modal is closed, focus does not return to the delete button."`
- **Remediation:** Implement proper modal focus management:
  ```tsx
  const [isOpen, setIsOpen] = useState(false);
  const triggerRef = useRef(null);
  const firstButtonRef = useRef(null);

  const openModal = () => {
    setIsOpen(true);
    setTimeout(() => firstButtonRef.current?.focus(), 0);
  };

  const closeModal = () => {
    setIsOpen(false);
    triggerRef.current?.focus();
  };

  return (
    <>
      <button ref={triggerRef} onClick={openModal}>
        Delete
      </button>
      {isOpen && (
        <div role="dialog" aria-label="Confirm deletion" aria-modal="true">
          <button ref={firstButtonRef}>Cancel</button>
          <button>Delete</button>
        </div>
      )}
    </>
  );
  ```

## External references

- wcag 2.1.1
- wcag 4.1.2
- section-508 502.3.2

Taxons: accessibility

HTML version: https://auditbuffet.com/patterns/ab-001608
