A Sliding Menu Effect Powered by Anchor Positioning
Anchor positioning is generally associated with tooltips and popovers, but the API opens the door to more creative UI patterns. This menu navigation demo shows how anchoring one element to another can drive a smooth hover effect — no JavaScript required.
Take note: only Chromium-based browsers fully support anchor positioning at the time of writing. View the demos in Chrome or Edge until support improves in Firefox and Safari.
The HTML and base styles
The structure is straightforward — a nav with an unordered list of links:
<nav>
<ul>
<li><a href="#">Home</a></li>
<li class="active"><a href="#">About</a></li>
<li><a href="#">Projects</a></li>
<li><a href="#">Blog</a></li>
<li><a href="#">Contact</a></li>
</ul>
</nav>
The exact semantics can vary by project, but the key is keeping a valid navigation structure. On the CSS side, basic styling creates a horizontal flex layout:
ul {
padding: 0;
margin: 0;
list-style: none;
display: flex;
gap: .5rem;
font-size: 2.2rem;
}
ul li a {
color: #000;
text-decoration: none;
font-weight: 900;
line-height: 1.5;
padding-inline: .2em;
display: block;
}
We remove default list styling and let Flexbox handle horizontal alignment.
How the sliding effect actually works
Visually, a blue rectangle appears to shrink to a sliver, move across to the hovered link, then grow back to full size. But the implementation involves more than one element. Each menu item has its own "element" that shrinks or grows, while a shared "element" — colored red in the annotated demo — actually slides between items. The background animation and the sliding animation are handled separately.
Animating the background
For the shrinking-and-growing effect, we animate the height of a CSS gradient:
/* 1 */
ul li {
background:
conic-gradient(lightblue 0 0)
bottom/100% 0% no-repeat;
transition: .2s;
}
/* 2 */
ul li:is(:hover,.active) {
background-size: 100% 100%;
transition: .2s .2s;
}
/* 3 */
ul:has(li:hover) li.active:not(:hover) {
background-size: 100% 0%;
transition: .2s;
}
The gradient uses a single-color syntax that initializes at 100% width and 0% height, anchored at the bottom. When a menu item is hovered or carries the .active class, the height jumps to 100% — with a delay inserted so the growth happens only after the shrink complets.
A dedicated selector handles a special case: when you hover an item that is not the active one, the .active item's gradient returns to 0% height. This creates the visual "shrink" phase for the previously selected item.
Handling the slide with anchor positioning
The second animation requires a single element that moves between items while adapting its width to fit each item's text. Anchor positioning handles this cleanly:
ul:before {
content:"";
position: absolute;
position-anchor: --li;
background: red;
transition: .2s;
}
ul li:is(:hover, .active) {
anchor-name: --li;
}
ul:has(li:hover) li.active:not(:hover) {
anchor-name: none;
}
Instead of adding extra markup, a pseudo-element on the ul is used as the sliding element. It is absolutely positioned and linked to whatever list item is current.
The anchor-name property marks a menu item as the anchor when it's hovered or active. Removing the name from the .active item when another item is hovered ensures only one anchor exists at a time.
The pseudo-element is then connected via position-anchor, using the same custom identifier --li. Note that anchor names must follow the <dashed-ident> syntax — always starting with two dashes.
With the anchor linked, the pseudo-element only needs dimensions:
ul:before {
bottom: anchor(bottom);
left: anchor(left);
right: anchor(right);
height: .2em;
}
The height value is trivial, but the anchor() function is new territory. As defined in the CSS spec:
The CSS
anchor()function takes an anchor element's side and resolves to the<length>where it is positioned. It can only be used in inset properties (e.g.top,bottom,left,right), normally to place an absolute-positioned element relative to an anchor.
So left: anchor(left) behaves like left: 0 but resolves against the anchor element rather than the containing block.
When you hover an item, it becomes the new anchor for the pseudo-element, and the anchor-based values update. The transition property ensures those changes animate smoothly rather than snapping into place.
An alternative syntax avoids the dedicated position-anchor property, embedding the anchor reference directly in the anchor() function:
ul:before {
content:"";
position: absolute;
inset: auto anchor(--li right) anchor(--li bottom) anchor(--li left);
height: .2em;
background: red;
transition: .2s;
}
Using a shared inset shorthand and repeated anchor names in each function call reads a bit redundant here, but it becomes valuable when an element must reference multiple different anchors simultaneously.
Combining both effects
Merging the gradient width animation with the sliding pseudo-element produces the full illusion. Sequencing relies on precise transition delays:
ul:before {
transition: .2s .2s;
}
ul li {
transition: .2s;
}
ul li:is(:hover,.active) {
transition: .2s .4s;
}
ul:has(li:hover) li.active:not(:hover) {
transition: .2s;
}
Three distinct phases occur in order: the gradient shrinks, the pseudo-element slides, and the gradient grows. The sliding animation uses a .2s delay (equal to one animation duration), while the growth phase delays by .4s — two full durations — so each phase runs in sequence without overlap.
Raising the bar with a bouncy variant
The same anchor positioning technique can drive more playful motion. Instead of a shrinking rectangle, the highlight morphs into a small circle, "jumps" to the next item, and reforms as a rectangle once it arrives.
This effect pairs two animations: each item's pseudo-element transforms its dimensions and border-radius for the morphing, while the ul pseudo-element animates as a small circle between targets. A second demo with contrasting colors and slower transitions exposes exactly what happens in each phase.
The jump itself relies on a custom cubic-bezier() curve. A dedicated article on advanced CSS animation with cubic Bézier curves breaks down that technique in detail.
Wrapping up
This experiment only touches a few anchor positioning capabilities — anchor-name to designate an anchor, position-anchor to link a target to it, and the anchor() function to position against the anchor's edges. Those three pieces are the foundation for much more complex interactions, opening up a new class of CSS-only UI animation.



