aria-modal
Indicates whether an element is modal when displayed. When true, background content is hidden from assistive technologies and focus is trapped within the dialog.
Overview
The aria-modal attribute indicates whether an element is modal. When set to true, assistive technologies should navigate only within the dialog, ignoring background content.
This is essential for accessible modal dialogs. Combined with proper focus management, it ensures screen reader users can't accidentally navigate to content behind the modal.
Focus Management Required
aria-modal="true" alone isn't enough. You must also implement focus trapping (Tab/Shift+Tab stay in modal) and keyboard handling (Escape to close).
Live Demo
Screen reader: "Modal Dialog Demo, dialog". With aria-modal="true", only content inside the dialog is accessible. Background is hidden from AT.
Code Examples
Basic Modal
<!-- Basic modal dialog with aria-modal -->
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<h2 id="modal-title">Confirm Action</h2>
<p id="modal-description">
Are you sure you want to delete this item?
</p>
<button>Cancel</button>
<button>Delete</button>
</div>
<!-- Screen reader: "Confirm Action, dialog" -->
<!-- Background content is hidden from AT when aria-modal="true" -->Modal vs Non-Modal
<!-- aria-modal=true vs aria-modal=false -->
<!-- MODAL dialog (aria-modal="true") -->
<div role="dialog" aria-modal="true" aria-label="Settings">
<!--
• Background content hidden from screen readers
• Focus is trapped inside the dialog
• User must dismiss to interact with page
• Use for: confirmations, alerts, forms
-->
</div>
<!-- NON-MODAL dialog (aria-modal="false" or absent) -->
<div role="dialog" aria-modal="false" aria-label="Help">
<!--
• Background content still accessible
• Focus can move outside the dialog
• User can interact with page while open
• Use for: tooltips, help panels, sidebars
-->
</div>Alert Dialog
<!-- Alert dialog (always modal) -->
<div
role="alertdialog"
aria-modal="true"
aria-labelledby="alert-title"
aria-describedby="alert-desc"
>
<h2 id="alert-title">⚠️ Warning</h2>
<p id="alert-desc">
You have unsaved changes. Leaving now will discard them.
</p>
<div class="button-group">
<button>Stay</button>
<button>Leave Anyway</button>
</div>
</div>
<!-- alertdialog implies modal behavior, but still add aria-modal -->React Component
// React Modal Component with aria-modal
import { useEffect, useRef, useCallback } from 'react';
import { createPortal } from 'react-dom';
function Modal({
isOpen,
onClose,
title,
children,
role = 'dialog' // or 'alertdialog'
}) {
const modalRef = useRef(null);
const previousFocus = useRef(null);
// Store focus and trap it
useEffect(() => {
if (isOpen) {
previousFocus.current = document.activeElement;
modalRef.current?.focus();
// Prevent body scroll
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
previousFocus.current?.focus();
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
// Handle keyboard events
const handleKeyDown = useCallback((e) => {
if (e.key === 'Escape') {
onClose();
return;
}
// Focus trap
if (e.key === 'Tab') {
const focusable = modalRef.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();
}
}
}, [onClose]);
if (!isOpen) return null;
return createPortal(
<>
{/* Backdrop */}
<div
className="modal-backdrop"
onClick={onClose}
aria-hidden="true"
/>
{/* Modal */}
<div
ref={modalRef}
role={role}
aria-modal="true"
aria-labelledby="modal-title"
tabIndex={-1}
onKeyDown={handleKeyDown}
className="modal"
>
<header className="modal-header">
<h2 id="modal-title">{title}</h2>
<button
onClick={onClose}
aria-label="Close dialog"
>
✕
</button>
</header>
<div className="modal-body">
{children}
</div>
</div>
</>,
document.body
);
}
// Usage
function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>
Open Settings
</button>
<Modal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title="Settings"
>
<p>Configure your preferences here.</p>
<button onClick={() => setIsOpen(false)}>
Save Changes
</button>
</Modal>
</>
);
}
