Skip to Content
Animate a Modal Without Breaking Keyboard Focus
Advanced
0 likes
Tutorials

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: none in your own CSS to hide the dialog. Leave the default alone.
  • Removing outline from the buttons inside the dialog.
  • Opening it with show(), which is not modal, and expecting focus trapping.
  • Forgetting a label. Use aria-labelledby so 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
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.