Skip to main content
お使いのブラウザは古いため、このサイトを正しく表示できません。Chrome、Edge、Firefox、Safari の最新版に更新してください。
ブログに戻る
DevelopmentKeyboardARIA

Building a modal dialog that works for keyboard and screen reader users

Most dialog bugs come down to focus: where it goes when the dialog opens, where it can go while it is open, and where it lands when it closes. How to get all three right with the native <dialog> element.

AccessiSight team
Accessibility engineering
2026年10月1日

A modal dialog is one of the few places where a web page takes control of the user's focus on purpose. Done well, keyboard and screen reader users barely notice. Done badly, they open a cookie settings panel and can't find it, or close it and get thrown back to the top of the page.

The three questions

Every dialog has to answer three questions, and most bugs are a wrong answer to one of them:

  1. When the dialog opens, where does focus go?
  2. While it's open, where can focus go?
  3. When it closes, where does focus land?

Start with the native element

The <dialog> element opened with showModal() answers the first two for you. MDN documents that the browser makes the rest of the page inert, sets focus "on the first nested focusable element" when it opens, and lets users dismiss it with the Esc key. It is also exposed to assistive technology as a modal dialog without extra ARIA.

<button type="button" id="open-settings">Cookie settings</button>

<dialog id="settings" aria-labelledby="settings-title">
  <h2 id="settings-title">Cookie settings</h2>
  <!-- the settings -->
  <button type="button" id="close-settings">Close</button>
</dialog>

aria-labelledby gives the dialog its accessible name, so a screen reader announces "Cookie settings, dialog" when it opens instead of just "dialog".

Control where focus starts

The first focusable element isn't always the right place to start. If the first thing in your dialog is a close button, users hear "Close, button" and have to guess what they're closing. Put autofocus on the element you want focused first, or put focus on the heading if the dialog is mostly text:

<h2 id="settings-title" tabindex="-1" autofocus>Cookie settings</h2>

Send focus back yourself

Don't count on the browser to return focus to the button that opened the dialog. Store it and return it explicitly. This works everywhere:

const dialog = document.getElementById('settings');
const opener = document.getElementById('open-settings');

opener.addEventListener('click', () => dialog.showModal());
document.getElementById('close-settings')
  .addEventListener('click', () => dialog.close());
dialog.addEventListener('close', () => opener.focus());

The close event fires however the dialog was closed (button, Esc key or form submission), so one handler covers every path.

If you can't use <dialog>

Some design systems still build dialogs from divs. Then you are responsible for everything the browser would have done: role="dialog" and aria-modal="true", an accessible name, moving focus in, keeping Tab inside (and making the background inert, which the inert attribute now does in all major browsers), closing on Esc, and returning focus. Miss one and the dialog breaks for someone.

Common failures we see

  • Focus stays behind the dialog. The dialog is on screen, but Tab moves through the page underneath it.
  • Focus isn't restored. After closing, focus drops to the top of the document and the user has to find their place again.
  • The background is still reachable, so Tab eventually escapes into content the user can't see.
  • No accessible name, so the dialog is announced without context.
  • A dialog taller than the screen that can't scroll, which cuts off the bottom for people who zoom in.

AccessiSight opens the dialogs it finds on a page and checks these behaviours in a real browser: whether focus moved in, whether it stays inside, whether it returns when the dialog closes, and whether the dialog has a name. It can only test dialogs it can open, so a dialog that only appears after a specific sequence (the third step of a checkout, say) still needs a manual check.

Further reading: MDN: the dialog element and the WAI-ARIA Authoring Practices modal dialog pattern.