Modal Open and Close with View Transitions
Replace fragile portal-based animation libraries with native View Transitions for a smooth modal open/close that respects DOM order and accessibility.
Detailed Explanation
The Modal Lifecycle in Three Phases
A polished modal animation needs to coordinate (1) the backdrop fading in, (2) the dialog scaling and fading in, and (3) the rest of the page subtly receding. The View Transitions API does (1) and (3) for free — you only need to author (2).
<dialog id="dlg" class="modal">…</dialog>
<button onclick="open()">Open</button>
<script>
function open() {
document.startViewTransition(() => {
document.getElementById('dlg').showModal();
});
}
</script>
.modal {
view-transition-name: app-modal;
}
@keyframes modal-out {
to { transform: scale(0.96) translateY(8px); opacity: 0; }
}
@keyframes modal-in {
from { transform: scale(0.96) translateY(8px); opacity: 0; }
}
::view-transition-old(app-modal) {
animation: 180ms ease-in both modal-out;
}
::view-transition-new(app-modal) {
animation: 240ms cubic-bezier(0.16, 1, 0.3, 1) both modal-in;
}
Why <dialog> plus View Transitions wins
The native <dialog> element handles focus trap, Esc to close, and scroll lock automatically. View Transitions handle the visual choreography. The combination replaces the entire React Modal / Headless UI overlay stack for most use cases — with about 12 lines of code instead of a dependency.
Closing animation
For close, call the same wrapper:
function close() {
document.startViewTransition(() => {
document.getElementById('dlg').close();
});
}
The ::view-transition-old(app-modal) rule fires automatically because the dialog disappears in the snapshot.
Accessibility: prefers-reduced-motion
Always wrap your custom keyframes in a media query so users who request reduced motion still get an instant swap with zero animation:
@media (prefers-reduced-motion: reduce) {
::view-transition-old(app-modal),
::view-transition-new(app-modal) {
animation-duration: 0ms !important;
}
}
Use Case
Replacing custom modal libraries (React Modal, Reach Dialog, Headless UI Dialog) when you want native animation, native accessibility (via <dialog>), and zero JavaScript overhead beyond a single startViewTransition call.