Popover Menus That Animate From Their Anchor

A menu that simply fades in above the page feels disconnected from the button that summoned it. A menu that grows out of that button — scaling up from the edge nearest its trigger — tells the user exactly where it came from and where it will go back to. The HTML popover attribute, anchor positioning, and the discrete-transition features together make this possible without a line of script: the popover opens from a button, positions itself against it, animates in and out, and closes when the user clicks elsewhere. This page focuses on the direction of that motion and the edge cases where it goes wrong. It belongs to Dialog & Popover Animations in the CSS-Only Micro-Interactions & Animations guide.

Why the origin of the motion matters

Motion carries spatial information. When an element scales up from a point, the eye reads that point as its source. A dropdown that scales from its own centre appears to materialise in mid-air; the same dropdown scaling from its top edge, just below the button, appears to unfold from the button. The second reads as cause and effect, which makes the interface feel more physical and easier to follow, especially when several menus are open in sequence.

The same principle governs the exit. A menu that shrinks back toward its trigger shows the user where to find it again. A menu that fades in place gives no such cue — acceptable, but less informative.

The popover API also brings behaviour that used to require script. popover="auto" gives light dismiss (click outside or press Escape to close), closes other auto popovers when a new one opens, places the popover in the top layer so no z-index or overflow can clip it, and returns focus sensibly. Animating it adds the spatial cue without giving any of that up.

Positioning and origin together

The popover is positioned relative to its button with anchor positioning, and its transform-origin is set to the edge that touches the button. When the popover sits below the button, the origin is its top edge; when it sits to the right, its left edge.

Origin at the anchor edge versus the centre Left, a popover below its button with transform-origin at the top edge: intermediate frames grow downward from the button. Right, the same popover scaling from its centre: intermediate frames appear to float in mid-air, detached from the button. Where the growth starts decides where it seems to come from Account ▾ transform-origin: top unfolds from the button Account ▾ transform-origin: center appears in mid-air

The complete implementation

Live demoPopover menu growing from its anchor button
Click Account: the menu unfolds downward from the button, then click outside or press Escape to watch it retract.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Popover menu from its anchor</title>
<style>
  body { font: 15px/1.5 system-ui, sans-serif; margin: 2rem; }

  .trigger {
    anchor-name: --account;              /* the popover positions against this */
    padding: 0.5rem 0.9rem;
    border: 1px solid #94a3b8;
    border-radius: 8px;
    background: transparent;
    font: inherit;
  }

  .menu {
    /* Anchor positioning: below the button, aligned to its left edge. */
    position-anchor: --account;
    position-area: block-end span-inline-end;
    margin: 0.4rem 0 0;
    /* If there is no room below, try above. */
    position-try-fallbacks: flip-block;

    min-width: 12rem;
    padding: 0.4rem;
    border: 1px solid #cbd5e1;
    border-radius: 10px;
    background: #ffffff;
    box-shadow: 0 12px 30px rgb(15 23 42 / 0.18);

    /* Grow from the edge touching the button. */
    transform-origin: top left;

    /* Closed state + exit transition (runs INTO this state). */
    opacity: 0;
    scale: 0.92;
    transition:
      opacity 140ms ease-in,
      scale 140ms ease-in,
      display 140ms allow-discrete,
      overlay 140ms allow-discrete;
  }

  .menu:popover-open {
    opacity: 1;
    scale: 1;
    transition:
      opacity 200ms cubic-bezier(0.22, 1, 0.36, 1),
      scale 200ms cubic-bezier(0.22, 1, 0.36, 1),
      display 200ms allow-discrete,
      overlay 200ms allow-discrete;
  }

  @starting-style {
    .menu:popover-open { opacity: 0; scale: 0.92; }
  }

  .menu a {
    display: block;
    padding: 0.45rem 0.6rem;
    border-radius: 6px;
    color: inherit;
    text-decoration: none;
  }
  .menu a:hover, .menu a:focus-visible { background: #eef2ff; }
  .trigger:focus-visible, .menu a:focus-visible { outline: 2px solid #4f46e5; outline-offset: 2px; }

  @media (prefers-reduced-motion: reduce) {
    .menu, .menu:popover-open { scale: 1; }
  }
</style>
</head>
<body>
  <button class="trigger" type="button" popovertarget="account-menu">Account ▾</button>
  <nav class="menu" id="account-menu" popover aria-label="Account">
    <a href="/profile/">Profile</a>
    <a href="/billing/">Billing</a>
    <a href="/sign-out/">Sign out</a>
  </nav>
</body>
</html>

The popovertarget attribute wires the button to the popover and toggles it without script; it also sets the correct aria-expanded state on the button automatically. The scale change is deliberately small, from 0.92 rather than zero. A menu that starts at zero scale spends most of its entry too small to read; a small scale change gives the directional cue and lets the content be legible almost immediately.

The key technique: origin must agree with placement

transform-origin: top left is correct only while the menu sits below the button with its left edges aligned. position-try-fallbacks: flip-block moves the menu above the button when there is no room below — near the bottom of the viewport, say — and at that point a top-left origin makes the menu grow away from the button, from its far edge. The origin does not follow the fallback.

Flip fallbacks and a fixed origin Near the bottom of the viewport the popover flips above its button. With transform-origin at the top, growth starts at the popover's top edge, far from the button. A subtle centre scale or a fade does not depend on which side the popover is on. After flip-block, a top origin points the wrong way viewport bottom edge ↓ flipped menu button origin at top: far from button Options when flips are likely scale 0.97 from the centre fade only, no scale translate a few px toward the button in each case

CSS has no widely supported way yet for an element to know which fallback was applied, so there are three practical strategies. If the menu almost never flips — a header menu at the top of the page — keep the directional origin. If it flips often — a menu in a table row that may be anywhere on screen — use a scale from the centre small enough (0.97) that its direction does not read, or a fade with no scale at all. And if the direction genuinely matters, define the fallbacks explicitly with @position-try rules, which can set their own margin but not transform-origin, and accept a fade for the flipped case. The positioning side of this is covered in Anchor Positioning Fallbacks.

Keyboard and screen-reader behaviour during the animation

The animation is purely visual, and it is worth confirming that it stays that way. When the popover opens from its button, the menu is in the accessibility tree and operable from the very first frame, even while its opacity is still near zero. A keyboard user who presses Tab immediately lands on the first link, which is correct: the user asked for the menu and should not have to wait for decoration to finish.

The reverse matters as well. During the exit transition the menu is still rendered, because display is deferred, but it is no longer open. Its links are removed from the tab order as soon as the popover closes, so a user tabbing quickly after pressing Escape moves on to the next element on the page rather than into a fading menu. If a design ever makes a closing menu look interactive for longer — a slow fade of several hundred milliseconds — users may try to click links that no longer respond, which is another reason to keep exits short.

Focus return is handled by the platform: when a popover opened by a popovertarget button closes via Escape, focus returns to that button. Test this in each supported browser, because focus behaviour around top-layer elements has been refined across releases, and a menu that leaves focus on <body> after closing is disorienting for keyboard users.

Variation: a small stagger for menu items

A menu whose items settle in quick succession is easier to scan than one that appears all at once — but only if the stagger is tiny. Using @starting-style on the items and a delay from a custom property gives a stagger without keyframes.

.menu a {
  transition: opacity 180ms ease-out, translate 180ms ease-out;
  transition-delay: calc(var(--i, 0) * 25ms);
}

@starting-style {
  .menu:popover-open a {
    opacity: 0;
    translate: 0 -0.25rem;
  }
}

/* Index each item; only the first four are staggered. */
.menu a:nth-child(2) { --i: 1; }
.menu a:nth-child(3) { --i: 2; }
.menu a:nth-child(n + 4) { --i: 3; }

@media (prefers-reduced-motion: reduce) {
  .menu a { transition: none; }
}

With 25 milliseconds per item capped at three steps, the whole menu is fully visible within about a quarter of a second — fast enough that a practised user never waits. The indexing technique is the same one developed in Staggered List Animations With Custom Properties. On exit, the items do not need their own transition; the whole menu fades together, which keeps dismissal crisp.

Browser support

The popover attribute is supported in Chrome and Edge 114+, Firefox 125+ and Safari 17+. Anchor positioning — anchor-name, position-anchor and position-area — is supported in Chrome and Edge 125+ (with position-area from 129), Firefox 147+ and Safari 26+; position-try-fallbacks in Chrome and Edge 128+, Firefox 147+ and Safari 26+. @starting-style is supported in Chrome and Edge 117+, Firefox 129+ and Safari 17.5+, and transition-behavior: allow-discrete in Chrome and Edge 117+, Firefox 129+ and Safari 17.4+. Without anchor positioning, a popover defaults to the centre of the viewport; declare a fallback position, or gate the menu layout with @supports (position-area: block-end).

FAQ

How do I make a popover appear to grow out of its button? Position the popover next to its button with anchor positioning, then set transform-origin to the edge nearest the button and animate scale and opacity from @starting-style. The popover then expands away from the button instead of from its own centre.

What happens to the animation when the popover flips to the other side?position-try-fallbacks can move the popover above the button when there is no room below, but transform-origin does not flip with it, so a top-origin growth would appear to come from the wrong edge. Use a subtle scale from the centre, or a fade only, when flipping is likely.

Can a popover be closed by clicking outside without JavaScript? Yes. popover="auto" provides light dismiss: clicking outside, pressing Escape, or opening another auto popover closes it. Its exit transition plays in each case because they all remove the :popover-open state.

Should menu items inside the popover be animated individually? A short stagger can help the eye scan a menu, but keep it tiny, around 20 to 30 milliseconds per item and only for the first few, or the last item appears too late to click. Skip it entirely for reduced-motion users.

Related articles

More pages in the same section.