position-try Fallbacks and Motion

Part of CSS Anchor Positioning & Motion in Modern View Transitions & Scroll APIs.

The problem

A dropdown menu opens below its button with a pleasant downward slide and a scale from its top edge. Near the bottom of the viewport, anchor positioning flips it above the button — correct placement — but it still slides downward toward the button and scales from its top edge, so it appears to grow away from the thing that opened it. When the page is scrolled so the button sits right at the threshold, the menu occasionally jumps between above and below as layout settles.

Fallbacks solve placement. They do not know anything about motion.

Root cause analysis: fallbacks are chosen in layout

The switch is not a style change. position-try-fallbacks lists alternatives the browser tries when the element overflows its containing block. The choice happens while laying out the element, after styles have been computed. Transitions are triggered by computed style changes, so moving from one fallback to another is not something a transition can see or interpolate. The element simply appears in the new position.

Motion defined on the base rule assumes the base position. An entrance of translate: 0 -8px to 0 0 slides down into place, which is right for a menu below its button and wrong for one above it. transform-origin: top scales from the edge nearest the button when below, and from the far edge when above.

@position-try can carry more than position. A named @position-try --above { … } rule may contain only a limited set of properties: inset properties, margins, sizing, self-alignment and position-anchor and position-area. It cannot set translate or transform-origin. So per-position motion cannot live directly in the fallback.

Flip-dependent motion needs a different signal. Two approaches work. Make motion direction-neutral, so it looks right in any position: a fade with a small uniform scale, transform-origin: center. Or use margin — which is allowed in @position-try — to create a gap, and animate a property that does not depend on direction. Where direction really matters, script can read the resolved position after opening and set a data attribute, at the cost of a frame.

Thrash near the threshold. An element sitting at the edge of fitting can flip as its own size changes — including during a scale entrance, when it is briefly smaller. Transforms do not affect layout, so a scale entrance does not change fallback choice; animating height or width would.

Directional against direction-neutral entrance after a flipThe menu has flipped above its button in both panels.Directional against direction-neutral entrance after a flipSlide down, scale from topMoves away from the buttonGrows from the edge far from the buttonLooks detachedWrong after flippingFade with scale from centreNo travel direction to get wrongGrows evenly in placeLooks attached in every positionRight in any position
The menu has flipped above its button in both panels.

Step-by-step resolution

Motion that survives fallbacksNeutral by default; directional only with a reliable signal.Motion that survives fallbacks1List fallbacks with position-try-fallbacks: flip-block, flip-inline.Placement handled by the browser2Replace directional slides with opacity and scale 0.96 to 1.Motion reads correctly anywhere3Set transform-origin: center, or omit it.No growth away from the anchor4Keep entrances on transform and opacity only.Animation never changes fallback choice5Where direction matters, read the resolved side after opening and set data-side.Per-side offsets from CSS6Under reduced motion, fade only.
Neutral by default; directional only with a reliable signal.

Production code pattern

.menu-button { anchor-name: --menu; }

.menu {
  position: absolute;
  inset: auto;
  margin: 0;
  position-anchor: --menu;
  position-area: block-end span-inline-end;
  margin-block-start: 6px;
  position-try-fallbacks: --above, flip-inline;
  opacity: 0;
  scale: 0.96;                                        /* direction-neutral */
  transition:
    opacity var(--motion-exit-duration) linear,
    scale var(--motion-exit-duration) var(--motion-exit-easing),
    display var(--motion-exit-duration) allow-discrete,
    overlay var(--motion-exit-duration) allow-discrete;
}
.menu:popover-open {
  opacity: 1;
  scale: 1;
  transition:
    opacity var(--motion-enter-duration) linear,
    scale var(--motion-enter-duration) var(--motion-enter-easing),
    display var(--motion-enter-duration) allow-discrete,
    overlay var(--motion-enter-duration) allow-discrete;
  @starting-style { opacity: 0; scale: 0.96; }
}

@position-try --above {
  position-area: block-start span-inline-end;
  margin-block-start: 0;
  margin-block-end: 6px;                              /* margins are allowed in @position-try */
}

/* Optional directional nuance, driven by a measured side. */
.menu[data-side="above"]:popover-open { @starting-style { translate: 0 4px; } }
.menu[data-side="below"]:popover-open { @starting-style { translate: 0 -4px; } }
.menu:popover-open { translate: 0 0; transition-property: opacity, scale, translate, display, overlay; }

@media (prefers-reduced-motion: reduce) {
  .menu, .menu:popover-open { scale: 1; translate: none; }
}
// Only if directional nuance is wanted: detect the resolved side on open.
menu.addEventListener('beforetoggle', (e) => {
  if (e.newState !== 'open') return;
  const b = button.getBoundingClientRect();
  const spaceBelow = innerHeight - b.bottom;
  menu.dataset.side = spaceBelow >= menu.offsetHeight + 12 ? 'below' : 'above';
});

Rendering Impact: composite for the entrance; layout once to resolve the anchored position and choose a fallback. Because only scale, translate and opacity animate, the entrance cannot cause the element to flip mid-animation.

The script predicts the side from available space rather than reading the resolved position after opening, so the attribute is set before the starting style applies. Its prediction can disagree with the browser’s choice in unusual layouts; the neutral scale-and-fade remains correct either way, and the 4px nuance is small enough that a rare mismatch is barely visible.

What can go inside @position-tryPlacement properties only; motion must be handled elsewhere.What can go inside @position-tryAllowed in @position-tryUseinset properties,position-areaYesPlacementmarginsYesGap toward the anchorwidth, height, minand max sizesYesFit in available spacetranslate,transform-originNoUse data attributes or neutral motionopacity, transitionNoBase or state rules
Placement properties only; motion must be handled elsewhere.

Verification checklist

Constraints and trade-offs

  • Transitions cannot interpolate between fallback positions.
  • @position-try accepts only positioning-related properties.
  • Predicting the side in script duplicates the browser’s decision and can disagree.
  • Direction-neutral motion conveys less about where an element came from.
  • Scroll-triggered fallback changes while open also snap.

Frequently asked questions

Can I animate an anchored element moving to its fallback position?

No. The fallback is chosen during layout rather than through a style change, so there is no transition to run. The element appears in the new position.

Why does my menu animate away from its button after flipping?

Its entrance uses a direction and transform-origin designed for the default position. Use a direction-neutral fade and scale, or set per-side offsets.

Can @position-try blocks contain transforms?

No. They only accept positioning-related properties such as insets, margins, sizes, alignment and position-area.

Can an entrance animation cause the element to flip?

Not if it only animates transforms and opacity, which do not affect layout. Animating width or height can change whether it fits.