Skip to main content
UI Patterns 65% fail

Dialog / Modal

Focus trap inside, Escape to close, focus returns to trigger on dismiss. Use <dialog> or role='dialog' + aria-modal='true'. One of the most commonly broken patterns.

In plain terms

Pop-up windows must keep keyboard focus inside while open, close with the Escape key, and return you to where you started.

The native HTML

element with showModal() handles most accessibility requirements automatically: focus trapping, Escape to close, background inertness, and proper role. It's the recommended approach over custom ARIA implementations.

If you must use a custom modal: set role="dialog" + aria-modal="true", trap focus with a focus-trap library, handle Escape, and return focus to the trigger element on close.

Why this matters

Modal dialogs fail on approximately 65% of sites that use them. Without proper focus management, keyboard users tab behind the modal into invisible content. Without Escape support, they can't dismiss it. Without focus return, they lose their place after closing.

How to detect

Quick check

Open the modal with keyboard (Enter/Space). Can you Tab through all elements inside? Does Tab wrap within the modal? Does Escape close it? After closing, does focus return to the trigger? Can you interact with content behind the modal?

How to fix

<!-- Modern: use native <dialog> element -->
<button id="open-btn">Open</button>
<dialog id="my-dialog">
  <h2>Dialog title</h2>
  <p>Content here.</p>
  <button autofocus>Confirm</button>
  <button id="close-btn">Cancel</button>
</dialog>

<script>
const dialog = document.getElementById('my-dialog');
const openBtn = document.getElementById('open-btn');
const closeBtn = document.getElementById('close-btn');

openBtn.addEventListener('click', () => dialog.showModal());
closeBtn.addEventListener('click', () => dialog.close());

dialog.addEventListener('close', () => openBtn.focus());
</script>

<!-- The <dialog> element handles: -->
<!-- ✓ Focus trapping -->
<!-- ✓ Escape to close -->
<!-- ✓ role="dialog" -->
<!-- ✓ Background inert -->