References

Beginner-friendly references for web development, with live, editable examples.

The HTML aria-modal attribute

ARIA Accessibility
In short

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

Live 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-modal dialog 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 with showModal() 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.

Frequently asked questions

Why can I still tab behind a dialog with aria-modal?
Focus can still leave because aria-modal adds no behavior. Make the background inert and keep focus inside the dialog, or open a native dialog element with showModal().
Should I add aria-modal to a native dialog element?
No, aria-modal isn't needed on a dialog opened with showModal(), though ARIA in HTML allows it. On one opened with show(), it's a false claim, because the dialog is then exposed as modal while the page stays usable.
Does aria-modal replace aria-hidden on background content?
Only in intent, because with aria-modal alone the background stays in the accessibility tree on Windows. Make it inert for real, which also stops focus and clicks from reaching it.
Is aria-modal valid on role=alertdialog?
Yes, the alertdialog role inherits aria-modal, so set it to true on a custom modal alert dialog. A dialog element can also take that role, and showModal() then supplies the modal state without the attribute.
When should aria-modal be false?
A dialog that leaves the page usable can set aria-modal to false or omit it. Either works, because false is the default.