⚡IntermediateAdvanced Accessibility
Accessible Modals and Dialogs
Build accessible modal dialogs using the native <dialog> element and ARIA attributes.
Overview
The HTML5 <dialog> element is the native solution for modal and non-modal dialogs. It handles accessibility automatically: focus trapping, Escape key to close, backdrop overlay, and the appropriate ARIA semantics.
For custom div-based dialogs, you must manually add: role="dialog", aria-modal="true", aria-labelledby referencing the dialog title, focus trapping, and Escape key handling.
Always prefer the native <dialog> element over custom implementations it's better supported and requires far less code.
Key Points
- •dialog.showModal() opens as modal (with backdrop, focus trap, Escape closes)
- •dialog.show() opens as non-modal (no backdrop, no focus trap)
- •dialog.close() closes the dialog; the close event fires
- •<dialog> has role="dialog" and aria-modal="true" automatically when using showModal()
- •aria-labelledby on <dialog> referencing the dialog's heading title
- •The CSS ::backdrop pseudo-element styles the overlay behind a modal dialog
Examples
Native <dialog> as a modal
<!-- The dialog element -->
<dialog id="confirm-dialog" aria-labelledby="dialog-title">
<h2 id="dialog-title">Confirm Delete</h2>
<p>Are you sure you want to delete this item? This action cannot be undone.</p>
<div class="dialog-actions">
<button id="cancel-btn">Cancel</button>
<button id="confirm-btn" class="danger">Delete</button>
</div>
</dialog>
<!-- Trigger button -->
<button id="delete-trigger">Delete Item</button>
<script>
const dialog = document.getElementById('confirm-dialog');
const trigger = document.getElementById('delete-trigger');
// Open modal
trigger.addEventListener('click', () => {
dialog.showModal();
});
// Close on cancel return focus to trigger
document.getElementById('cancel-btn').addEventListener('click', () => {
dialog.close('cancel');
});
// Close on confirm
document.getElementById('confirm-btn').addEventListener('click', () => {
dialog.close('confirm');
// Handle deletion...
});
// Return focus to trigger when dialog closes
dialog.addEventListener('close', () => {
trigger.focus();
});
</script>
<style>
/* Overlay styling */
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}
</style>Reading the dialog return value
dialog.addEventListener('close', () => {
console.log(dialog.returnValue); // "cancel" or "confirm"
if (dialog.returnValue === 'confirm') {
deleteItem();
}
});dialog.close(value) sets dialog.returnValue useful for conditional post-close logic.
Custom div dialog with required ARIA (if native dialog is not feasible)
<div
id="custom-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="custom-dialog-title"
tabindex="-1"
hidden
>
<h2 id="custom-dialog-title">Edit Profile</h2>
<form>
<label for="name">Name</label>
<input type="text" id="name" name="name">
<button type="submit">Save</button>
<button type="button" id="custom-close">Cancel</button>
</form>
</div>You must manually add focus trapping, Escape handling, and backdrop overlay.
Common Mistakes
- Using show() instead of showModal() no focus trap or Escape key closes
- Not adding aria-labelledby screen readers announce the dialog without context
- Forgetting to return focus to the trigger on close keyboard users are stranded
Note
Use the native <dialog> element with showModal() it provides focus trapping and Escape key closing for free. The only customization needed is CSS for the dialog and ::backdrop styling.
Exercises
Write HTML to solve each exercise. Use Preview to see it render in a live iframe, then mark as done.
Loading editor…
Completed: 0 / 5 exercises
Knowledge Check
Question 1 of 4