CSS Anchor Positioning `position-try-fallbacks` — How to Automatically Adjust Dropdown Position in 4 Directions Without JavaScript
Honestly, I relied on Floating UI or Popper.js for dropdown positioning calculations for quite a long time. Reading coordinates with getBoundingClientRect() and writing a loop with requestAnimationFrame to recalculate on every scroll felt like the natural course of action when panels got clipped at viewport edges. It wasn't until I did a bundle size analysis and saw Floating UI alone weighing in at over 30KB that I started wondering, "Does this really have to be JavaScript?"
By leveraging position-try-fallbacks in CSS Anchor Positioning, you can declaratively handle everything from viewport boundary detection to 4-directional automatic flipping and dropdown toggling — without a single line of JavaScript. When Firefox 147 joined in January 2026, it achieved Baseline 2026 status and now works natively in over 91% of browser traffic. I've written up the patterns you can use to replace a component right now, along with the trial and error I went through myself.
Core Concepts
The Three Layers of CSS Anchor Positioning
Anchor positioning is built around three main stages. When I first encountered it, the property names were all so similar that I got quite confused, but thinking in terms of roles makes it much easier to understand.
| Property | Role | Applies To |
|---|---|---|
anchor-name |
Assigns an identifier to the reference element | Anchor (reference) element |
position-anchor + position-area |
Declares which anchor to attach to, and in which direction | Positioned target element |
position-try-fallbacks |
List of fallback positions the browser tries in order when overflow occurs | Positioned target element |
position-area is a high-level abstraction that declares placement direction using grid-based directional keyword combinations. There are various values ranging from basic directions like top, bottom, left, right to combinations like span-inline and span-block, and it's far more intuitive than using the anchor() function directly, making position-area the natural first choice in practice.
position-try-fallbacks — Telling the Browser "Try These in Order"
When a positioned element overflows beyond the viewport or a specified container, the browser tries fallback positions one by one in order and adopts the first value that doesn't overflow. There are two ways to specify values.
① Built-in <try-tactic> keywords
.tooltip {
position: fixed;
position-anchor: --btn;
position-area: top;
/* Tries: top → bottom → left-right flip → top-bottom + left-right simultaneous flip */
position-try-fallbacks:
flip-block,
flip-inline,
flip-block flip-inline;
}| Keyword | Behavior |
|---|---|
flip-block |
Physical flip on the block axis (top ↔ bottom) |
flip-inline |
Physical flip on the inline axis (left ↔ right) |
flip-start |
Logical start ↔ end flip based on writing direction |
flip-block flip-inline |
Separated by a space, both flip simultaneously (quadrant switch) |
Unlike flip-block/flip-inline, flip-start flips the writing-direction-relative "start point" rather than a physical direction. In RTL languages like Arabic, the inline start is on the right, making this useful for multilingual layouts. For projects without RTL, the flip-block/flip-inline combination covers most cases.
Automatic RTL/LTR handling:
flip-blockandflip-inlineare based on logical properties. The block direction refers to the direction text wraps, and the inline direction refers to the direction text flows — both determined by the writing mode. This means they flip correctly in RTL layouts like Arabic without any additional handling.
② @position-try custom blocks
For complex positions that are difficult to express with built-in keywords (size changes, margin adjustments, etc.), you can register named blocks.
@position-try --left-side {
position-area: left;
width: 200px;
margin-inline-end: 8px;
}
@position-try --right-side {
position-area: right;
width: 200px;
margin-inline-start: 8px;
}
.dropdown {
position-try-fallbacks: --left-side, --right-side, flip-block;
}position-try-order — Choosing by Available Space, Not Order
By default, position-try-fallbacks tries values in list order. But by combining it with position-try-order, you can change the algorithm to prefer the position with "the most available space."
.panel {
position-try-order: most-height; /* Prioritize position with most vertical space */
position-try-fallbacks: flip-block, flip-inline;
}Note: Values like
most-heightandmost-widthmay be recalculated on every scroll. If you use this property in complex layouts, it's recommended to run performance profiling first.
Now that we've covered the concepts, it's time to see how they work in real situations. Here are four scenarios I frequently encounter in practice, covered in order.
Practical Application
Example 1: A Dropdown Panel That Doesn't Clip at Any Corner of the Viewport
We've all experienced it at least once — a menu button on the far right of a header causes the dropdown to overflow to the right, and a button near the bottom of the screen gets clipped below. I used to handle this with JS by calculating coordinates and dynamically toggling classes, but now it looks like this.
If you need a CSS-only toggle, the <details>/<summary> combination is the cleanest approach. The browser handles opening and closing, while anchor positioning is only responsible for placement.
<details class="nav-menu">
<summary class="menu-trigger">Menu ▾</summary>
<div class="dropdown-panel">
<ul>
<li>Profile</li>
<li>Settings</li>
<li>Logout</li>
</ul>
</div>
</details>.menu-trigger {
anchor-name: --nav-menu;
cursor: pointer;
list-style: none; /* Remove default details triangle */
}
.dropdown-panel {
position: fixed;
position-anchor: --nav-menu;
position-area: bottom span-right; /* Default: below the button, right-aligned */
position-try-fallbacks:
flip-block, /* ① Bottom of screen → move above button */
flip-inline, /* ② Right of screen → switch to left-aligned */
flip-block flip-inline; /* ③ Bottom+right corner → position upper-left */
width: max-content;
margin-block-start: 4px;
}| Try Order | Condition | Result |
|---|---|---|
Default (bottom span-right) |
Sufficient space | Below button, right-aligned |
flip-block |
Insufficient space below | Move above button |
flip-inline |
Insufficient space to the right | Switch to left-aligned |
flip-block flip-inline |
Insufficient space below and to the right | Position upper-left of button |
Note:
<details>does not automatically close when clicking outside (no Light Dismiss). If you need that behavior, the Popover API pattern in Example 2 is more suitable.
I initially had flip-block and flip-inline in reverse order, and experienced the panel awkwardly snapping to the left on mobile portrait screens because horizontal flipping was tried first. Since vertical space shortage occurs far more often than horizontal space shortage on mobile portrait, putting flip-block first is better for user experience.
Example 2: A Fully JavaScript-free Dropdown Combined with the Popover API
This is the pattern I've used most this year. The role separation is very clean — HTML's popover attribute handles toggling, layering, and keyboard behavior, while CSS anchor positioning handles placement.
By connecting the button and popover with popovertarget and id, the browser automatically registers the button as the implicit anchor for that popover. Thanks to this, positioning works correctly without needing to explicitly declare anchor-name or position-anchor in CSS.
<button id="menu-btn" popovertarget="nav-dropdown">
Menu ▾
</button>
<ul id="nav-dropdown" popover>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>#nav-dropdown {
/* Anchor reference created automatically just by linking popovertarget ↔ id — no anchor-name needed */
position: fixed;
position-area: bottom span-right;
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
margin: 0;
padding: 8px 0;
list-style: none;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: white;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}What the Popover API provides:
- Close with Esc key — default browser behavior
- Light Dismiss — automatically closes when clicking outside
- Top-layer stacking — end of
z-indexwars - Automatic ARIA role management — built-in accessibility handling
top-layer: A special rendering layer managed by the browser that always sits above the
z-indexstacking context.dialogandpopoverelements render here.
Example 3: Escaping an overflow: hidden Container
Placing a dropdown inside a scrollable table or card list and having it clipped by the container is something I also handled with JS for quite a while. The old approach was to calculate coordinates with getBoundingClientRect() and hardcode the values directly into position: fixed. Now it looks like this.
/* Scroll container */
.table-wrapper {
overflow: auto;
height: 400px;
}
/* Action button inside a row */
.row-action-btn {
anchor-name: --row-action;
}
/* Dropdown — rendered outside the overflow container with position: fixed */
.row-dropdown {
position: fixed;
position-anchor: --row-action;
position-area: bottom span-right;
position-try-fallbacks: flip-block, flip-inline;
/* Automatically hidden when anchor scrolls out of the viewport */
position-visibility: anchors-visible;
}
position-visibility: anchors-visible: When the anchor element scrolls out of the viewport and becomes invisible, the target element is also automatically hidden. This solves the problem of dropdowns floating in mid-air while scrolling with a single line of CSS.
Example 4: Custom Multi-directional Tooltips
There's an important caveat when building tooltips with ::after pseudo-elements. When multiple elements share the same anchor-name value, only the last declared element is referenced as the anchor. This means the second and subsequent tooltips will attach to the wrong position. You must give each element a unique anchor-name and ensure the ::after element's position-anchor references that name.
@position-try --tooltip-left {
position-area: left;
margin-inline-end: 8px;
}
@position-try --tooltip-right {
position-area: right;
margin-inline-start: 8px;
}
/* Give each button a unique anchor-name */
.btn-save { anchor-name: --btn-save; }
.btn-delete { anchor-name: --btn-delete; }
/* Common tooltip styles */
.btn-save::after,
.btn-delete::after {
content: attr(data-tooltip);
position: fixed;
position-area: top;
margin-block-end: 8px;
/* Tries: top → left → right → bottom */
position-try-fallbacks:
--tooltip-left,
--tooltip-right,
flip-block;
background: #1f2937;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.875rem;
white-space: nowrap;
pointer-events: none;
}
/* Ensure each ::after references only its own anchor */
.btn-save::after { position-anchor: --btn-save; }
.btn-delete::after { position-anchor: --btn-delete; }<button class="btn-save" data-tooltip="Saves the file">Save</button>
<button class="btn-delete" data-tooltip="This cannot be undone">Delete</button>For repeated components: For dynamically rendered elements like list items, it's recommended to assign inline
style="anchor-name: --tooltip-N"via JS, or to use the Popover API's implicit anchor reference.
Pros and Cons Analysis
Advantages
- Performance: Processed by the browser's layout engine — no
requestAnimationFrameloops orgetBoundingClientRect()reflows. - Simplified code: Removing Floating UI / Popper.js dependencies noticeably reduces bundle size.
- Accessibility: Combined with the Popover API, the browser provides focus trapping, keyboard dismissal, and ARIA roles by default.
- Logical properties:
flip-block/flip-inlineautomatically account for RTL/LTR writing direction. - Declarative: Expressing placement intent in CSS alone makes the code much easier to understand when reading it later.
Disadvantages and Caveats
| Item | Details | Mitigation |
|---|---|---|
| Legacy browsers | Baseline 2026, but may require polyfills depending on project support range | @supports (anchor-name: --x) { } conditional branching + polyfill in parallel |
| Shadow DOM limitations | Cross-boundary anchor references are currently incomplete per spec | Place both anchor and target elements inside the component |
| Virtualized lists | Reference breaks when the anchor element is unmounted from the DOM | Combine with JS solution or use position-visibility |
| Complex nested menus | Highly complex cases like dynamically loaded submenus | JS still has the advantage here |
position-try-order performance |
most-height etc. may be recalculated on every scroll |
Use only after performance profiling confirms it's needed |
| Legacy property names | Chrome 125–128 uses the old name position-try-options |
Use position-try-fallbacks targeting Chrome 129+ |
Baseline: A compatibility metric jointly managed by the W3C and browser vendors, defining "Baseline" as the point where Chrome/Edge/Firefox/Safari all provide stable support. CSS Anchor Positioning became Baseline 2026 with Firefox's addition in January 2026.
The Most Common Mistakes in Practice
-
Using anchor positioning without
position: fixed— To escape anoverflow: hiddencontainer, you must useposition: fixedtogether.position: absolutecannot break out of a parent container. -
Guessing the order of
position-try-fallbacksvalues — The browser tries the list in order, so putting the most frequently occurring overflow direction first is better for user experience. On mobile portrait screens,flip-blockshould be tried beforeflip-inline. -
Multiple elements sharing the same
anchor-namevalue — When the sameanchor-nameis declared on multiple elements, only the last declared element is referenced. Repeated components require a unique identifier for each instance.
Closing Thoughts
CSS Anchor Positioning is not just about adjusting dropdown placement — it's a new way for CSS to directly express the relationship that "this element needs to be next to that element." I read it as a signal that the browser's layout engine is starting to take over a portion of the layout relationships we previously relied on JavaScript for. The area where Floating UI was "essential" is gradually becoming "optional."
Three steps you can start with right now:
-
Check browser support — Writing new code inside
@supports (anchor-name: --x) { }blocks safely isolates it from older browsers. Try enabling the anchor positioning relationship visualization in the Chrome DevTools Elements panel (Chrome 141+) — it makes debugging much easier. -
Replace the simplest case first — If your project has a tooltip implemented with
getBoundingClientRect()+ class toggling, you can start by replacing it with the pattern from Example 4 above. It works with just adata-tooltipattribute and CSS — no JavaScript needed. -
Set a polyfill strategy — If you need to support older browsers, you can add OddBird's
css-anchor-positioningpolyfill or the official Floating UI polyfill without@supportsconditions. Modern browsers will use native CSS, and the JS polyfill only kicks in for older ones.
References
If you're just getting started, these three are the best places to look first: MDN (property reference), CSS-Tricks (pattern examples), Ahmad Shadeed (visual explanations).
- position-try-fallbacks — MDN Web Docs
- @position-try — MDN Web Docs
- Fallback options and conditional hiding for overflow — MDN
- CSS Anchor Positioning Guide — CSS-Tricks
- position-try-fallbacks — CSS-Tricks Almanac
- Anchor Positioning — web.dev Learn CSS
- Introducing the CSS Anchor Positioning API — Chrome for Developers
- Anchor Positioning Updates for Fall 2025 — OddBird
- The Basics of Anchor Positioning — Ahmad Shadeed
- CSS Anchor Positioning Module Level 1 — W3C
- Anchor position fallbacks with position-try-fallbacks — Fractaled Mind
- The convenience of CSS's new @position-try — DEV Community
- CSS Anchor Positioning: Replace Floating UI With CSS — Botmonster Tech
- Understanding CSS Anchor Positioning and Fallback Detection — JavaScript in Plain English