The HTML aria-modal attribute
Adding aria-modal="true" to a custom dialog tells assistive technology that the content behind it isn't available. It blocks nothing itself, so your code has to make the background inert and manage focus. A native <dialog> opened with showModal() does that and needs no attribute.
Overview
The aria-modal value true flags an element as modal, meaning other content can't be used while it's displayed. It's defined on the abstract window role, which authors shouldn't use, so in practice it belongs on dialog and alertdialog.
The attribute adds no blocking of its own. With role="dialog" and aria-modal="true" alone, a button behind the dialog still takes focus and clicks, and on Windows the background stays in the accessibility tree.
The rest is up to you. While a modal element is displayed, the interface must be controllable using only its descendants, so a close button belongs inside the dialog. All other content should also be marked inert.
WAI-ARIA 1.2 gives assistive technology instructions too. Assistive technology should navigate to the modal element unless focus was explicitly set elsewhere, and it may limit navigation to the element's contents. Once focus moves outside the element, it should not keep that limit.
A native <dialog> handles the inert part for you. While a showModal() dialog is open, the rest of the document is inert, and the dialog is exposed as aria-modal="true". Inert content also drops out of the accessibility tree.
Syntax
<div role="dialog" aria-modal="true" aria-labelledby="title">
<h2 id="title">Delete this file?</h2>
<button>Cancel</button>
</div>
Values
| Value |
|---|
| true | false |
Example
<div id="page">
<p style="margin:0 0 8px">Open the panel each way, then try Save draft while it's open.</p>
<button type="button" id="open-modal">Open as modal</button>
<button type="button" id="open-plain">Open as non-modal</button>
<button type="button" id="save">Save draft</button>
<p role="status" id="state" style="margin:8px 0">Panel closed. Saved 0 times.</p>
</div>
<div role="dialog" id="panel" aria-labelledby="panel-title" style="padding:8px;border:1px solid #767676;border-radius:6px" hidden>
<strong id="panel-title">Share this draft</strong>
<p id="panel-note" style="margin:4px 0 8px"></p>
<button type="button" id="share">Mark as shared</button>
<button type="button" id="close">Close</button>
</div>
<script>
const page = document.getElementById('page');
const panel = document.getElementById('panel');
const state = document.getElementById('state');
let saves = 0;
let opener = null;
function report(text) {
state.textContent = text + ' Saved ' + saves + (saves === 1 ? ' time.' : ' times.');
}
function show(modal, button) {
opener = button;
if (modal) panel.setAttribute('aria-modal', 'true');
else panel.removeAttribute('aria-modal');
report(modal ? 'Modal panel open.' : 'Panel open.');
page.inert = modal;
page.style.opacity = modal ? '0.5' : '1';
document.getElementById('panel-note').textContent = modal
? 'aria-modal is true and the page behind is inert.'
: 'aria-modal is absent and the page behind still works.';
panel.hidden = false;
panel.querySelector('button').focus();
}
function hide(text) {
panel.hidden = true;
panel.removeAttribute('aria-modal');
page.inert = false;
page.style.opacity = '1';
opener.focus();
report(text);
}
document.getElementById('open-modal').addEventListener('click', function () { show(true, this); });
document.getElementById('open-plain').addEventListener('click', function () { show(false, this); });
document.getElementById('share').addEventListener('click', function () { hide('Draft marked as shared.'); });
document.getElementById('close').addEventListener('click', function () { hide('Panel closed.'); });
document.getElementById('save').addEventListener('click', function () {
saves++;
report(panel.hidden ? 'Panel closed.' : 'Panel open.');
});
document.addEventListener('keydown', function (event) {
if (panel.hidden) return;
const modal = panel.hasAttribute('aria-modal');
if (event.key === 'Escape' && (modal || panel.contains(document.activeElement))) hide('Panel closed.');
if (event.key === 'Tab' && modal) {
event.preventDefault();
const items = [...panel.querySelectorAll('button')];
const index = items.indexOf(document.activeElement);
const step = event.shiftKey ? -1 : 1;
items[index === -1 ? 0 : (index + step + items.length) % items.length].focus();
}
});
</script>
Best practices
- Give each
aria-modaldialog an accessible name with aria-labelledby or aria-label. WAI-ARIA 1.2 requires an accessible name on every dialog, and the 1.3 Working Draft relaxes that to a recommendation. - Move focus into the dialog when it opens, keep it there, and return it to the opener when the dialog closes.
- Keep a custom dialog outside the element you mark inert. The inert attribute applies to an element's descendants too, so a dialog nested inside the page wrapper would go inert with it.
- Add Escape handling to a custom dialog, because the attribute brings none. A native
<dialog>opened withshowModal()closes on a close request, such as the Esc key on desktop platforms.
Accessibility
Focus isn't the only way out of an aria-modal dialog. With focus trapped, screen reader users may still reach outside content by virtual cursor. The platform mapping differs too, giving Windows and Linux a modal state and dropping the background from the tree on macOS.























