-
Notifications
You must be signed in to change notification settings - Fork 26
Agent: Modal Specialist
Handles everything about overlays that appear above page content. Focus trapping, focus return, escape behavior, heading structure, background inertia, and scrolling behavior.
- Modals and dialogs
- Confirmation prompts
- Drawers and slide-out panels
- Popovers and tooltips
- Alert dialogs
- Cookie consent banners
- Any overlay that requires focus management
Expand - 8 modal issues detected
- Focus not trapped inside the modal
- Focus not returning to the trigger on close
- Escape key not closing the modal
- Missing
aria-modal="true"or<dialog>usage - Background content still interactive (not using
inert) - Heading level wrong (must start at H2 inside modals)
- Auto-focus landing on the wrong element
- Nested modals without proper stack management
Content issues inside the modal (form accessibility is forms-specialist, contrast is contrast-master). It owns the modal container behavior, not the content within it.
In Claude Code:
/modal-specialist review the confirmation dialog in CheckoutModal.tsx
/modal-specialist build an accessible drawer component
/modal-specialist is focus trapping correct in this modal?
In GitHub Copilot Chat:
@modal-specialist review this dialog for focus management
@modal-specialist build a cookie consent banner that meets WCAG
@modal-specialist check the drawer component in this file
When you ask the modal-specialist to review a dialog, here is the exact checklist it works through.
The five behaviors every modal must have:
1. Focus moves into the modal on open. When the modal opens, focus must move inside it - either to the first focusable element, the close button, or the dialog heading, depending on the context. Focus staying behind on the trigger is a critical failure.
2. Focus is trapped inside the modal.
While the modal is open, pressing Tab must cycle through only the modal's interactive elements. Focus must never reach the content behind the overlay. The correct, modern implementation uses the inert attribute on everything outside the modal, rather than manual focus trap event listeners.
3. Escape closes the modal.
No exceptions. Every modal must close on Escape. This is a hard standard from WCAG Success Criterion 2.1.2 (No Keyboard Trap). If the modal is a destructive action, it may ask for confirmation, but it must respond to Escape.
4. Focus returns to the trigger on close. When the modal closes, focus returns to the element that opened it. Without this, keyboard users are left disoriented in the page.
5. Background content is not interactive.
While the modal is open, links, buttons, and form elements behind the overlay must not be reachable. Use inert on the background container; do not rely only on pointer-events: none (that only affects mouse, not keyboard).
Additional checks:
-
<dialog>element withshowModal()is preferred over custom<div role="dialog"> -
aria-modal="true"when using the<dialog>element - Heading level: The first heading inside a modal should be H2 (the page H1 still exists in the background)
- Auto-focus: The correct element should receive initial focus based on the modal's purpose
// Before - common broken pattern
function Modal({ onClose }) {
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal">
<button onClick={onClose}>X</button> {/* No accessible label */}
<h1>Confirm delete</h1> {/* Wrong heading level */}
<p>Are you sure?</p>
<button onClick={onClose}>Cancel</button>
<button>Delete</button>
{/* No focus trap, no Escape handler, no inert on background */}
</div>
</div>
);
}
// After - correct implementation
function Modal({ onClose, triggerRef }) {
const dialogRef = useRef(null);
useEffect(() => {
document.getElementById('page-content').inert = true;
dialogRef.current.showModal(); // Native focus trap + Escape
return () => {
document.getElementById('page-content').inert = false;
triggerRef.current?.focus(); // Return focus to trigger
};
}, []);
return (
<dialog ref={dialogRef} aria-modal="true" aria-labelledby="dialog-title">
<h2 id="dialog-title">Confirm delete</h2>
<p>Are you sure?</p>
<button onClick={onClose}>Cancel</button>
<button>Delete</button>
<button onClick={onClose} aria-label="Close dialog"></button>
</dialog>
);
}| Connect to | When |
|---|---|
| forms-specialist | When the modal contains a form - the specialist reviews the form content separately |
| keyboard-navigator | For complex focus management scenarios and tab order within the modal |
| aria-specialist | For ARIA attributes on the dialog element itself (aria-labelledby, aria-describedby) |
| live-region-controller | For modals that display dynamic status messages after submission |
| accessibility-lead | For full audits that combine the modal with its surrounding page context |
Show example prompts
/modal-specialist review the confirmation dialog in CheckoutModal.tsx
/modal-specialist build an accessible drawer component
/modal-specialist is focus trapping correct in this modal?
/modal-specialist audit all dialogs in this project
@modal-specialist review this dialog for focus management
@modal-specialist build a cookie consent banner that meets WCAG
@modal-specialist check the drawer component in this file
Expand constraints
- Requires
<dialog>withshowModal()as the preferred implementation. Accepts custom implementations only when<dialog>is genuinely insufficient - Requires focus to return to the trigger element on close - no exceptions
- Will reject modals that can only be closed by clicking outside (must have Escape support)
- Validates both the opening and closing flows
- Accessibility Lead
- Web Accessibility Wizard
- Document Accessibility Wizard
- Alt Text and Headings
- ARIA Specialist
- Contrast Master
- Forms Specialist
- Keyboard Navigator
- Link Checker
- Live Region Controller
- Modal Specialist
- Tables Data Specialist
- Word Accessibility
- Excel Accessibility
- PowerPoint Accessibility
- PDF Accessibility
- Office Scan Config
- PDF Scan Config
- Testing Coach
- WCAG Guide