Navbar Active State Morphing with View Transitions
WEB PLATFORMS

Navbar Active State Morphing with View Transitions

Personal Project
FRONTEND DEVELOPER
Dec 2024 – Present

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:

  1. Unique names for each nav link — Allows the browser to track each link’s position
  2. 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:

  1. Old page snapshot captures nav-active-bg at the “Work” link position
  2. New page renders nav-active-bg at the “Background” link position
  3. Browser detects matching transition:name and creates a geometry morph
  4. 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.

::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:

BenefitMechanism
Spatial orientationUsers understand “Background is to the right of Work”
Reduced cognitive loadEye naturally tracks the moving element
Perceived qualitySmall polish details compound into premium feel
AccessibilityMotion 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;
}
}