Animate a Modal Without Breaking Keyboard Focus
Use a native dialog, animate it with CSS only, and check focus, Tab, Escape and reduced motion by keyboard.
A modal that fades in but loses keyboard focus is a broken modal. The native <dialog> element handles focus for you, so the work is to animate it without taking that away.
Step 1: use <dialog> and showModal()
<button id="open" type="button">Delete account</button>
<dialog id="dlg" aria-labelledby="t">
<h2 id="t">Delete this account?</h2>
<p>This cannot be undone.</p>
<form method="dialog">
<button id="cancel" value="cancel">Cancel</button>
<button id="confirm" value="confirm">Delete</button>
</form>
</dialog>
<script>
const dlg = document.getElementById("dlg");
document.getElementById("open").addEventListener("click", () => dlg.showModal());
</script>
showModal() puts the dialog on the top layer, makes the rest of the page inert, moves focus into the dialog, and lets Escape close it. <form method="dialog"> closes it from a button without any script.
Step 2: animate it with CSS only
dialog {
opacity: 0;
transform: translateY(12px);
transition: opacity 200ms ease, transform 200ms ease,
overlay 200ms allow-discrete, display 200ms allow-discrete;
}
dialog[open] { opacity: 1; transform: none; }
@starting-style { dialog[open] { opacity: 0; transform: translateY(12px); } }
dialog::backdrop {
background: rgb(8 11 14 / 0%);
transition: background 200ms ease, overlay 200ms allow-discrete, display 200ms allow-discrete;
}
dialog[open]::backdrop { background: rgb(8 11 14 / 60%); }
@starting-style { dialog[open]::backdrop { background: rgb(8 11 14 / 0%); } }
@media (prefers-reduced-motion: reduce) {
dialog, dialog::backdrop { transition-duration: 1ms; }
dialog { transform: none; }
}
@starting-style gives the entry animation its starting values. Adding display and overlay with allow-discrete keeps the dialog on screen long enough to play the exit animation. @starting-style is Baseline newly available (August 2024), so check your audience's browsers before you rely on it. In an older browser the dialog still works, it just appears without the fade.
Step 3: check the keyboard path
I drove it with the keyboard in Chrome 154:
after open: focus on cancel (dialog open: true)
Tab x4 inside: confirm, body, cancel, confirm
after Escape: open: false focus back on open display: none
reduced motion: transition-duration 0.001s normal: 0.2s
Focus moved into the dialog on open and stayed there. Tab went from Cancel to Delete, then past the last control (the active element was the body), then back to Cancel. It never reached the "Delete account" button behind the dialog. Escape closed it and focus returned to the button that opened it. With reduced motion on, the transition is 1 ms.
⚠ Warning: Do not animate the dialog with visibility or height tricks on a div and call it a modal. You will lose focus trapping, Escape, the inert background and the top layer, and you will have to rebuild each one by hand.
Common mistakes
- Setting
display: nonein your own CSS to hide the dialog. Leave the default alone. - Removing
outlinefrom the buttons inside the dialog. - Opening it with
show(), which is not modal, and expecting focus trapping. - Forgetting a label. Use
aria-labelledbyso it has a name.
Related course: HTML and CSS Advanced.
About
Use a native dialog, animate it with CSS only, and check focus, Tab, Escape and reduced motion by keyboard.
- Responsible
- Jay P
- Last Update
- 08/14/2026
- Reading Time
- 3 min
Reviews
Sign in to write a reviewComments (0)
Sign in to commentNo comments yet.