Overview
Modal dialogs are one of the trickiest components to get right. A visually convincing modal can be completely broken for keyboard and screen reader users if it lacks the dialog role, a focus trap, and proper labeling. When a modal opens, focus must move into it. When it closes, focus must return to the element that triggered it.
WCAG Criteria:
- 2.1.2 No Keyboard Trap — focus must be contained within the modal while it is open
- 2.4.3 Focus Order — focus must move to the dialog when it opens and return on close
- 4.1.2 Name, Role, Value — the dialog must have a role and an accessible name
Key requirements:
- Use
role="dialog"(or<dialog>element) - Add
aria-modal="true"to tell screen readers to ignore content outside - Label the dialog with
aria-labelledbypointing to the heading inside it - Move focus to the first focusable element (or the dialog itself) on open
- Trap Tab/Shift+Tab within the dialog
- Close on Escape key
- Return focus to the trigger element on close
Modal Dialog
Accessible Dialog vs. Unsemantic Modal Div
Inaccessible
<!-- No dialog role, no focus management, no aria-modal -->
<button onclick="document.getElementById('bad-modal').style.display='flex'">
Delete Account
</button>
<div id="bad-modal" style="display:none; position:fixed; inset:0;
background:rgba(0,0,0,0.5); justify-content:center; align-items:center;">
<div style="background:white; padding:24px; border-radius:8px;">
<h2>Are you sure?</h2>
<p>This action cannot be undone.</p>
<button onclick="document.getElementById('bad-modal').style.display='none'">
Cancel
</button>
<button>Delete</button>
</div>
</div>Live Preview
Accessible
<button id="modal-trigger" onclick="openModal()">Delete Account</button>
<div
id="good-modal"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-desc"
tabindex="-1"
hidden
style="position:fixed; inset:0; display:flex; justify-content:center; align-items:center;"
>
<div style="background:white; padding:24px; border-radius:8px;">
<h2 id="modal-title">Are you sure?</h2>
<p id="modal-desc">This action cannot be undone.</p>
<button id="modal-cancel" onclick="closeModal()">Cancel</button>
<button onclick="closeModal()">Delete</button>
</div>
</div>
<script>
function openModal() {
var modal = document.getElementById('good-modal');
modal.removeAttribute('hidden');
modal.focus();
// trap focus...
}
function closeModal() {
var modal = document.getElementById('good-modal');
modal.setAttribute('hidden', '');
document.getElementById('modal-trigger').focus();
}
</script>Live Preview