Skip to content

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 TypeDuration
Tooltips150–200ms
Dropdown menus200–300ms
Modal dialogs300–400ms
Hover cards250–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-name on 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