Over 14 years I have lost far too many hours to leaky focus traps in custom modals. We used to write big JavaScript utilities, or pull in a library, purely to stop a user tabbing back into the header while a popup was open. That is no longer the job. Writing your own logic to trap focus in the HTML Dialog element now means fighting the browser instead of using it.
While refactoring a client’s “accessible” modal component I hit something odd. Testing the <dialog> element’s showModal() method, I could tab straight out of the dialog and land on the browser’s address bar. My first reaction was that it was broken. After reading the current specs, the broken part turned out to be my own mental model of accessibility.
The focus trap was a workaround, not a rule
For years a strict focus trap was the gold standard: the user should not be able to leave the modal until it closed. That made sense when modals were <div> tags wearing ARIA attributes, because we had to hide the rest of the page from assistive technology ourselves.
Plenty of developers still treat the W3C APG patterns as law. Those patterns are good, but they were written before the HTML Dialog element and the inert attribute had broad support. I made the same argument in my post on why semantic HTML is often enough: the browser handles these edge cases better than our scripts do.
Why tabbing to the address bar is fine
The current consensus from the W3C’s Accessible Platform Architectures (APA) Working Group is that showModal() should not confine focus to the web content. A keyboard user tabbing into the browser chrome, meaning the address bar, the tabs and the settings, is valid behavior.
That escape route is a usability win. Someone can open a new tab to look something up, or change a setting, without being stuck inside your modal’s focus logic. Their browser belongs to them, not to your component.
The code you can delete
This is the kind of legacy code I still find in WordPress themes and custom plugins. It is fragile, tedious to maintain, and with the HTML Dialog element it does nothing you need.
// THE OLD, MESSY WAY (Don't do this with <dialog>)
const trapFocus = (e, modal) => {
const focusableEls = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
const firstEl = focusableEls[0];
const lastEl = focusableEls[focusableEls.length - 1];
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstEl) {
lastEl.focus();
e.preventDefault();
} else if (!e.shiftKey && document.activeElement === lastEl) {
firstEl.focus();
e.preventDefault();
}
}
};
The version in the MDN Dialog documentation replaces those 30 lines with a handful. Call showModal() and the browser marks the background content inert on its own.
// THE NEW, PRAGMATIC WAY
const bbioon_modal = document.querySelector('#my-dialog');
const bbioon_openBtn = document.querySelector('#open-btn');
bbioon_openBtn.addEventListener('click', () => {
// This native method handles focus management and
// background blocking without custom JS traps.
bbioon_modal.showModal();
});
Two things that still go wrong
If focus is leaking to elements behind the modal, check whether you called .show() instead of .showModal(). The first one gives you a plain non-modal popup. The second promotes the dialog to the top layer and switches on the native focus behavior you want.
Styling trips people up too. The ::backdrop pseudo-element only applies when the dialog was opened with showModal(), so a missing backdrop usually points back at the method call. On the CSS side, I also wrote about how to style search text for better accessibility.
If this is eating your billable hours, I can take it on. I have been working with WordPress since the 4.x days.
Where that leaves your modals
Delete the focus trap and let showModal() do the work. There is less code to maintain, and users get the standard behavior rather than your approximation of it. Then go spend the saved hours on a problem that is actually yours.