Appearance
Animate Overlay from Trigger
Use View Transitions API to morph overlays from their trigger element, creating spatial continuity that reinforces cause-and-effect.
When to use
Use this technique for any overlay that should appear to emerge from its trigger — modal dialogs growing from buttons, dropdown menus from nav items, tooltips from icons, or context menus from the click location.
The pattern
Temporarily assign the same view-transition-name to both the trigger and the overlay, wrap the DOM update in document.startViewTransition(), then clean up.
Opening a modal dialog
Call showOverlay with the trigger button and the dialog element, then open the dialog after the transition starts:
javascript
editButton.addEventListener("click", async () => {
await showOverlay(editButton, editDialog);
editDialog.showModal();
});
closeButton.addEventListener("click", async () => {
editDialog.close();
await hideOverlay(editButton, editDialog);
});Opening a dropdown menu
Animate the menu from the button, then dismiss it when the user clicks outside:
javascript
menuButton.addEventListener("click", async () => {
await showOverlay(menuButton, dropdownMenu);
});
document.addEventListener("click", async (e) => {
if (
!menuButton.contains(e.target) &&
!dropdownMenu.contains(e.target)
) {
await hideOverlay(menuButton, dropdownMenu);
}
});Showing a tooltip
Animate the tooltip from the hovered element on mouseenter and reverse it on mouseleave:
javascript
element.addEventListener("mouseenter", async () => {
await showOverlay(element, tooltip);
});
element.addEventListener("mouseleave", async () => {
await hideOverlay(element, tooltip);
});Implementing showOverlay
The function assigns a shared transition name, starts the view transition to show the overlay, and cleans up after completion:
javascript
async function showOverlay(trigger, overlay) {
if (!document.startViewTransition) {
overlay.style.display = "block";
return;
}
const transitionName = "overlay-morph";
trigger.style.viewTransitionName = transitionName;
overlay.style.viewTransitionName = transitionName;
const transition = document.startViewTransition(() => {
overlay.style.display = "block";
});
await transition.finished;
trigger.style.viewTransitionName = "";
overlay.style.viewTransitionName = "";
}Implementing hideOverlay
This mirrors showOverlay but hides the overlay instead:
javascript
async function hideOverlay(trigger, overlay) {
if (!document.startViewTransition) {
overlay.style.display = "none";
return;
}
const transitionName = "overlay-morph";
trigger.style.viewTransitionName = transitionName;
overlay.style.viewTransitionName = transitionName;
const transition = document.startViewTransition(() => {
overlay.style.display = "none";
});
await transition.finished;
trigger.style.viewTransitionName = "";
overlay.style.viewTransitionName = "";
}Styling the transition
Set duration and easing on the view transition pseudo-elements:
css
::view-transition-old(overlay-morph),
::view-transition-new(overlay-morph) {
animation-duration: 300ms;
animation-timing-function: ease-in-out;
}Recommended durations vary by overlay type:
| Overlay Type | Duration |
|---|---|
| Tooltips | 150–200ms |
| Dropdown menus | 200–300ms |
| Modal dialogs | 300–400ms |
| Hover cards | 250–350ms |
Browser support
Chrome 111+, Edge 111+, Safari 18+, and Firefox 144+ support the View Transitions API. Always include feature detection with the fallback shown in the implementations above.
Validation checklist
Verify each implementation covers these points:
- Same
view-transition-nameon trigger and overlay - Names are temporary (cleaned after transition)
- Both open and close animate
- Feature detection for unsupported browsers
- Unique names when multiple overlays exist