
Navbar Active State Morphing with View Transitions
A NAVBAR THAT FEELS ALIVE—THE ACTIVE INDICATOR SLIDES BETWEEN ITEMS DURING PAGE TRANSITIONS.
Navbar Active State Morphing with View Transitions
The Active State Problem
Standard navigation updates instantly—click a link, and the active indicator jumps to its new position. This works, but misses an opportunity: the View Transitions API can make the indicator slide between positions during page navigation, reinforcing spatial relationships in the interface.
This guide covers the implementation patterns for shared-element navbar transitions.
Pattern 1: Shared Element Identity
The View Transitions API morphs elements that share the same transition:name across pages. For a sliding active indicator, we need:
- Unique names for each nav link — Allows the browser to track each link’s position
- A shared name for the active background — The background “moves” to whichever link is active
<nav class="flex items-center gap-4"> {navItems.map((item) => ( <a href={item.href} transition:name={`nav-link-${item.id}`}> {isActive(item.href) && ( <span class="nav-active-bg" transition:name="nav-active-bg" /> )} <span class="relative z-10">{item.label}</span> </a> ))}</nav>How the Morph Works
On navigation from /work to /background:
- Old page snapshot captures
nav-active-bgat the “Work” link position - New page renders
nav-active-bgat the “Background” link position - Browser detects matching
transition:nameand creates a geometry morph - The background animates from old position/size to new position/size
No manual position calculations needed—the browser handles the interpolation.
Pattern 2: Animation Timing Control
The default morph animation uses a linear interpolation that may not match your design language. Override with CSS:
Position Morphing (Automatic)
The browser automatically animates:
- X/Y position
- Width/height
- Border-radius
Opacity and Scale (Manual)
Add polish with custom entry/exit animations:
@keyframes scale-up { from { transform: scale(0.85); opacity: 0; } to { transform: scale(1); opacity: 1; }}
@keyframes scale-down { from { transform: scale(1); opacity: 1; } to { transform: scale(0.85); opacity: 0; }}
::view-transition-group(nav-active-bg) { animation-duration: 0.35s; animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); z-index: 50;}
::view-transition-old(nav-active-bg) { animation: scale-down 0.2s ease-out;}
::view-transition-new(nav-active-bg) { animation: scale-up 0.3s ease-out;}This creates a subtle shrink-grow effect layered on top of the position morph.
Pattern 3: Entry and Exit Animations
When navigating to/from pages without the navbar, the element appears or disappears rather than morphing. The :only-child pseudo-selector targets these cases:
Navbar Entry (Appearing for First Time)
::view-transition-new(main-navbar):only-child { animation: navbar-snap-down 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;}
@keyframes navbar-snap-down { from { transform: translateY(-100%); } to { transform: translateY(0); }}The :only-child condition means “there is no corresponding old snapshot”—the element exists only on the destination page.
Navbar Exit (Disappearing)
::view-transition-old(main-navbar):only-child { animation: navbar-snap-up 0.3s ease-in forwards;}
@keyframes navbar-snap-up { from { transform: translateY(0); } to { transform: translateY(-100%); }}Active Indicator Entry/Exit
The same pattern applies to the active background when navigating to a page with no active nav item:
::view-transition-old(nav-active-bg):only-child { animation: scale-down 0.2s ease-out;}
::view-transition-new(nav-active-bg):only-child { animation: scale-up 0.3s ease-out;}Pattern 4: Visual Stability Preservation
View transitions can introduce visual glitches if not carefully managed.
The Flex-Wrap Shiver
Navigation bars using flex-wrap may briefly recalculate wrap points during the snapshot phase, causing a visible “shiver.” Force horizontal layout:
<nav class="flex flex-nowrap overflow-x-auto"> <!-- Prevents mid-transition reflow --></nav>Cursor State Inheritance
Transition snapshots don’t inherit cursor styling by default. Interactive elements lose their pointer cursor during the animation:
::view-transition-old(*),::view-transition-new(*),::view-transition-image-pair(*) { cursor: inherit;}Button State Reset
If clicking a nav item while it’s in hover/active state, the snapshot captures that state. This can cause the button to appear “stuck” during the transition:
html:has(::view-transition) .btn { transition: none !important; transform: translate(0, 0) !important; box-shadow: var(--btn-shadow) !important;}This forces buttons to their neutral visual state during transitions.
Pattern 5: Z-Index Layering
The active background must sit behind the link text:
.nav-active-bg { position: absolute; inset: 0; z-index: 0; background-color: var(--accent-color); border-radius: inherit; pointer-events: none;}
nav a { position: relative;}
nav a > span:last-child { position: relative; z-index: 10;}Complete CSS Reference
/* Per-link transition groups */::view-transition-group(nav-link-work),::view-transition-group(nav-link-approach),::view-transition-group(nav-link-background),::view-transition-group(nav-link-contact) { animation-duration: 0.25s; animation-timing-function: ease-out;}
/* Active background morph */::view-transition-group(nav-active-bg) { animation-duration: 0.35s; animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); z-index: 50;}
/* Entry/exit animations */::view-transition-old(nav-active-bg),::view-transition-new(nav-active-bg) { mix-blend-mode: normal;}
::view-transition-old(nav-active-bg):only-child { animation: scale-down 0.2s ease-out;}
::view-transition-new(nav-active-bg):only-child { animation: scale-up 0.3s ease-out;}
/* Navbar entry */::view-transition-new(main-navbar):only-child { animation: navbar-snap-down 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;}
/* Navbar exit */::view-transition-old(main-navbar):only-child { animation: navbar-snap-up 0.3s ease-in forwards;}UX Considerations
A morphing active indicator provides:
| Benefit | Mechanism |
|---|---|
| Spatial orientation | Users understand “Background is to the right of Work” |
| Reduced cognitive load | Eye naturally tracks the moving element |
| Perceived quality | Small polish details compound into premium feel |
| Accessibility | Motion respects prefers-reduced-motion |
For users with prefers-reduced-motion, disable the morph:
@media (prefers-reduced-motion: reduce) { ::view-transition-group(*) { animation-duration: 0.01ms !important; }}Related
- Building an Astro Portfolio with AI-Assisted Development — Parent project overview
- Persistent Filter Bar with View Transitions — Scroll position and hover state preservation