CSS Anchor Positioning
CSS Anchor Positioning lets you position elements relative to an anchor element— any element you designate as a positioning reference. This eliminates the need for JavaScript-based tooltip and popover libraries by giving CSS a native way to say "position this element relative to that one."
The feature introduces the anchor-name property on anchor elements and the position-anchor / anchor() function on positioned elements. Combined with position-area, it provides a complete system for building tooltips, popovers, dropdown menus, and floating UI — all in CSS.
| 1 | /* Define an anchor */ |
| 2 | .tooltip-trigger { |
| 3 | anchor-name: --my-trigger; |
| 4 | } |
| 5 | |
| 6 | /* Position a tooltip relative to that anchor */ |
| 7 | .tooltip { |
| 8 | position: fixed; |
| 9 | position-anchor: --my-trigger; |
| 10 | top: anchor(bottom); |
| 11 | left: anchor(center); |
| 12 | transform: translateX(-50%); |
| 13 | margin-top: 8px; |
| 14 | } |
An anchor element is any element that has the anchor-name property set. This name is a dashed identifier (similar to CSS custom properties) that referenced elements use to find their anchor.
| 1 | /* Any element can be an anchor */ |
| 2 | .button { |
| 3 | anchor-name: --submit-btn; |
| 4 | } |
| 5 | |
| 6 | .card { |
| 7 | anchor-name: --product-card; |
| 8 | } |
| 9 | |
| 10 | .nav-item { |
| 11 | anchor-name: --nav-home; |
| 12 | } |
| 13 | |
| 14 | /* Multiple anchors on the same element */ |
| 15 | .input-wrapper { |
| 16 | anchor-name: --email-field --name-field; |
| 17 | } |
Anchor names are scoped to the document. An element with position: absolute, fixed, or sticky can reference any anchor by name. The anchor element must exist in the DOM at render time.
note
The position-anchor property establishes the default anchor reference for an element. Once set, you can use the anchor() function and position-area to position relative to it.
| 1 | /* Simple tooltip pattern */ |
| 2 | .trigger { |
| 3 | anchor-name: --trigger; |
| 4 | } |
| 5 | |
| 6 | .tooltip { |
| 7 | position: fixed; |
| 8 | position-anchor: --trigger; |
| 9 | |
| 10 | /* Position below the trigger, centered */ |
| 11 | position-area: bottom center; |
| 12 | |
| 13 | /* Or use anchor() for precise placement */ |
| 14 | top: anchor(bottom); |
| 15 | left: anchor(center); |
| 16 | transform: translateX(-50%); |
| 17 | } |
| 18 | |
| 19 | /* Implicit anchor — when a positioned element is a descendant |
| 20 | of an anchor, you can skip position-anchor */ |
| 21 | .parent { |
| 22 | anchor-name: --parent; |
| 23 | } |
| 24 | |
| 25 | .child { |
| 26 | position: absolute; |
| 27 | /* Automatically finds --parent as its anchor */ |
| 28 | position-area: top right; |
| 29 | } |
Two functions provide precise control: anchor() for positioning and anchor-size() for sizing. They let you reference any edge or the center of the anchor element.
anchor() Function
| 1 | /* Reference anchor edges */ |
| 2 | .tooltip { |
| 3 | position: fixed; |
| 4 | position-anchor: --trigger; |
| 5 | |
| 6 | /* Top edge of tooltip at bottom edge of anchor */ |
| 7 | top: anchor(bottom); |
| 8 | |
| 9 | /* Left edge at the anchor's left edge */ |
| 10 | left: anchor(left); |
| 11 | |
| 12 | /* Center of tooltip at center of anchor */ |
| 13 | top: anchor(center); |
| 14 | left: anchor(center); |
| 15 | transform: translate(-50%, -50%); |
| 16 | |
| 17 | /* With offset distance */ |
| 18 | top: anchor(bottom 8px); /* 8px below the bottom edge */ |
| 19 | left: anchor(right 12px); /* 12px to the right of right edge */ |
| 20 | } |
| 21 | |
| 22 | /* Different edge names */ |
| 23 | .left-tooltip { |
| 24 | position-anchor: --trigger; |
| 25 | right: anchor(left 8px); /* 8px to the left */ |
| 26 | top: anchor(top); |
| 27 | } |
| 28 | |
| 29 | .right-tooltip { |
| 30 | position-anchor: --trigger; |
| 31 | left: anchor(right 8px); /* 8px to the right */ |
| 32 | top: anchor(top); |
| 33 | } |
anchor-size() Function
| 1 | /* Match the width of the anchor */ |
| 2 | .trigger { |
| 3 | anchor-name: --trigger; |
| 4 | } |
| 5 | |
| 6 | .dropdown { |
| 7 | position: fixed; |
| 8 | position-anchor: --trigger; |
| 9 | |
| 10 | /* Match anchor width */ |
| 11 | width: anchor-size(width); |
| 12 | |
| 13 | /* Match anchor height */ |
| 14 | height: anchor-size(height); |
| 15 | |
| 16 | /* Fraction of anchor size */ |
| 17 | min-width: anchor-size(width) * 0.8; |
| 18 | max-height: anchor-size(height) * 3; |
| 19 | |
| 20 | /* Position below */ |
| 21 | top: anchor(bottom); |
| 22 | left: anchor(left); |
| 23 | } |
The position-area shorthand is a grid-based system for placing elements relative to their anchor. It uses a 3×3 grid notation where you specify which cells the positioned element should occupy.
| 1 | /* position-area grid layout: |
| 2 | |
| 3 | top left | top center | top right |
| 4 | left | center | right |
| 5 | bottom left | bottom center | bottom right |
| 6 | |
| 7 | */ |
| 8 | |
| 9 | /* Below and centered */ |
| 10 | .tooltip { |
| 11 | position-anchor: --trigger; |
| 12 | position-area: bottom center; |
| 13 | } |
| 14 | |
| 15 | /* Above and centered */ |
| 16 | .tooltip-above { |
| 17 | position-anchor: --trigger; |
| 18 | position-area: top center; |
| 19 | } |
| 20 | |
| 21 | /* To the right, full height */ |
| 22 | .sidebar-tooltip { |
| 23 | position-anchor: --trigger; |
| 24 | position-area: right; |
| 25 | } |
| 26 | |
| 27 | /* Bottom-right corner */ |
| 28 | .floating-widget { |
| 29 | position-anchor: --container; |
| 30 | position-area: bottom right; |
| 31 | } |
info
One of the most powerful features is automatic fallback positioning. When the preferred position would cause the element to overflow the viewport, the browser shifts it to a fallback position. You specify fallbacks using the position-try-fallbacks property.
| 1 | .tooltip { |
| 2 | position: fixed; |
| 3 | position-anchor: --trigger; |
| 4 | |
| 5 | /* Preferred position */ |
| 6 | top: anchor(bottom 8px); |
| 7 | left: anchor(center); |
| 8 | transform: translateX(-50%); |
| 9 | |
| 10 | /* Fallbacks — browser tries each in order */ |
| 11 | position-try-fallbacks: |
| 12 | --above, /* Custom try tactics */ |
| 13 | --left-side, |
| 14 | flip-block, /* Built-in: flip vertically */ |
| 15 | flip-inline; /* Built-in: flip horizontally */ |
| 16 | } |
| 17 | |
| 18 | /* Define custom try tactics */ |
| 19 | @position-try --above { |
| 20 | top: anchor(top 8px); |
| 21 | bottom: auto; |
| 22 | transform: translateX(-50%); |
| 23 | } |
| 24 | |
| 25 | @position-try --left-side { |
| 26 | right: anchor(left 8px); |
| 27 | left: auto; |
| 28 | top: anchor(center); |
| 29 | transform: translateY(-50%); |
| 30 | } |
Built-in fallback keywords simplify common patterns. flip-block mirrors the element along the block axis, and flip-inline mirrors along the inline axis. These are shorthand for common @position-try definitions.
| 1 | /* Built-in flip fallbacks */ |
| 2 | .tooltip { |
| 3 | position-anchor: --trigger; |
| 4 | top: anchor(bottom 8px); |
| 5 | left: anchor(center); |
| 6 | transform: translateX(-50%); |
| 7 | |
| 8 | /* If it would overflow bottom, flip to top */ |
| 9 | position-try-fallbacks: flip-block; |
| 10 | } |
| 11 | |
| 12 | /* Custom @position-try block */ |
| 13 | @position-try --compact { |
| 14 | width: 200px; |
| 15 | padding: 8px; |
| 16 | font-size: 12px; |
| 17 | } |
| 18 | |
| 19 | .tooltip { |
| 20 | position-anchor: --trigger; |
| 21 | top: anchor(bottom 8px); |
| 22 | left: anchor(left); |
| 23 | position-try-fallbacks: flip-block, --compact; |
| 24 | } |
Here is a complete, production-ready tooltip pattern using CSS Anchor Positioning. No JavaScript required.
| 1 | <button class="tooltip-trigger" aria-describedby="tooltip-1"> |
| 2 | Hover me |
| 3 | </button> |
| 4 | |
| 5 | <div id="tooltip-1" class="tooltip" role="tooltip"> |
| 6 | This is a CSS-only tooltip |
| 7 | </div> |
| 1 | .tooltip-trigger { |
| 2 | anchor-name: --tooltip-trigger; |
| 3 | } |
| 4 | |
| 5 | .tooltip { |
| 6 | position: fixed; |
| 7 | position-anchor: --tooltip-trigger; |
| 8 | top: anchor(bottom 8px); |
| 9 | left: anchor(center); |
| 10 | transform: translateX(-50%); |
| 11 | |
| 12 | /* Visual styling */ |
| 13 | background: #1A1A2E; |
| 14 | color: #E0E0E0; |
| 15 | font-size: 12px; |
| 16 | padding: 8px 12px; |
| 17 | border-radius: 6px; |
| 18 | border: 1px solid #333; |
| 19 | white-space: nowrap; |
| 20 | pointer-events: none; |
| 21 | |
| 22 | /* Show/hide with opacity */ |
| 23 | opacity: 0; |
| 24 | transition: opacity 0.15s; |
| 25 | } |
| 26 | |
| 27 | .tooltip-trigger:hover + .tooltip, |
| 28 | .tooltip-trigger:focus + .tooltip { |
| 29 | opacity: 1; |
| 30 | } |
| 31 | |
| 32 | /* Fallback: flip above if near viewport bottom */ |
| 33 | .tooltip { |
| 34 | position-try-fallbacks: flip-block; |
| 35 | } |
Combine CSS Anchor Positioning with the HTML popover attribute for a powerful, accessible popover system. The popover API handles open/close state while CSS handles positioning.
| 1 | <button popovertarget="menu-1">Open Menu</button> |
| 2 | |
| 3 | <div id="menu-1" popover anchor="anchor-menu"> |
| 4 | <nav> |
| 5 | <a href="/profile">Profile</a> |
| 6 | <a href="/settings">Settings</a> |
| 7 | <a href="/logout">Log Out</a> |
| 8 | </nav> |
| 9 | </div> |
| 1 | /* Anchor element */ |
| 2 | button[popovertarget] { |
| 3 | anchor-name: --menu-anchor; |
| 4 | } |
| 5 | |
| 6 | /* Popover styling */ |
| 7 | [popover] { |
| 8 | position: fixed; |
| 9 | position-anchor: --menu-anchor; |
| 10 | top: anchor(bottom 4px); |
| 11 | left: anchor(left); |
| 12 | |
| 13 | /* Size relative to anchor */ |
| 14 | min-width: anchor-size(width); |
| 15 | |
| 16 | /* Visual styling */ |
| 17 | background: #1A1A2E; |
| 18 | border: 1px solid #333; |
| 19 | border-radius: 8px; |
| 20 | padding: 8px; |
| 21 | box-shadow: 0 8px 24px rgba(0,0,0,0.3); |
| 22 | |
| 23 | /* Animation */ |
| 24 | opacity: 0; |
| 25 | transform: translateY(-4px); |
| 26 | transition: opacity 0.15s, transform 0.15s; |
| 27 | } |
| 28 | |
| 29 | [popover]:popover-open { |
| 30 | opacity: 1; |
| 31 | transform: translateY(0); |
| 32 | } |
| 33 | |
| 34 | /* Fallback for overflow */ |
| 35 | [popover] { |
| 36 | position-try-fallbacks: flip-block; |
| 37 | } |
CSS Anchor Positioning is a newer feature with growing browser support. Check current status before using in production.
| Browser | Support | Version |
|---|---|---|
| Chrome | ✓ | 125+ |
| Edge | ✓ | 125+ |
| Firefox | ◐ | In development |
| Safari | ◐ | In development |
warning