View Transitions API

Smooth, native page transitions using the browser's View Transitions API.

Overview

MiniWork uses the View Transitions API to create smooth animations between page navigations without a full page reload. This provides a native app-like experience.

🔄

Automatic

Transitions happen automatically on link clicks

↕️

Directional

Vertical slides for sidebar navigation

↔️

Horizontal

Horizontal slides for section changes

60fps

GPU-accelerated CSS animations

How It Works

  1. User clicks a link
  2. JavaScript intercepts the click and determines direction
  3. document.startViewTransition() captures the old state
  4. New page content is fetched and swapped
  5. CSS animations transition between old and new states

Direction Detection

The transition direction is determined by navigation context:

NavigationDirection
Admin: Dashboard → Users → Roles → SettingsDown (vertical)
Admin: Settings → DashboardUp (vertical)
Docs: Getting Started → Core → APIDown (vertical)
Home → Docs → AdminForward (horizontal)
Admin → HomeBack (horizontal)

CSS Implementation

/* Define transition names for different areas */
.admin-sidebar { view-transition-name: admin-sidebar; }
.admin-header { view-transition-name: admin-header; }
.admin-content { view-transition-name: admin-content; }

/* Static elements - no animation */
::view-transition-old(admin-sidebar),
::view-transition-new(admin-sidebar) {
  animation: none;
}

/* Vertical slide animations for content */
@keyframes slide-down-out {
  to { transform: translateY(30px); opacity: 0; }
}
@keyframes slide-down-in {
  from { transform: translateY(-30px); opacity: 0; }
}

html.vt-down::view-transition-old(admin-content) {
  animation: 0.15s ease-out both slide-down-out;
}
html.vt-down::view-transition-new(admin-content) {
  animation: 0.15s ease-out both slide-down-in;
}

JavaScript Handler

document.addEventListener('click', async (e) => {
  const link = e.target.closest('a[href]');
  if (!link || !document.startViewTransition) return;
  
  e.preventDefault();
  const direction = getDirection(currentPath, link.href);
  
  document.documentElement.classList.add('vt-' + direction);
  
  const transition = document.startViewTransition(async () => {
    const html = await fetch(link.href).then(r => r.text());
    // Swap content...
  });
  
  await transition.finished;
  document.documentElement.classList.remove('vt-' + direction);
});
Browser Support

View Transitions API is supported in Chrome 111+, Edge 111+, and Safari 18+. The code includes fallbacks for unsupported browsers.

Cross-Section Navigation Gotcha

Orphaned View Transition Elements

When an element has a view-transition-name but doesn't exist on the target page, the browser creates a transition group with only the 'old' state. This causes a glitchy fade-out effect even if animation: none is set.

This commonly occurs when navigating from a page with a sidebar (docs, admin) to a page without one (landing page). The sidebar fades out late and looks broken.

The Fix: Remove view-transition-name Before Transition

Before starting the view transition, detect cross-section navigation and remove the view-transition-name from elements that won't exist on the target page:

// Before document.startViewTransition()
const isLeavingDocs = currentPath.startsWith('/docs') && !toPath.startsWith('/docs');
const isLeavingAdmin = currentPath.startsWith('/admin') && !toPath.startsWith('/admin');

if (isLeavingDocs) {
  const sidebar = document.querySelector('.docs-sidebar');
  if (sidebar) sidebar.style.viewTransitionName = 'none';
}

if (isLeavingAdmin) {
  const sidebar = document.querySelector('.admin-sidebar');
  const header = document.querySelector('.admin-header');
  const footer = document.querySelector('.admin-footer');
  if (sidebar) sidebar.style.viewTransitionName = 'none';
  if (header) header.style.viewTransitionName = 'none';
  if (footer) footer.style.viewTransitionName = 'none';
}

// Now start the transition
const transition = document.startViewTransition(async () => {
  // fetch and swap content...
});

Setting viewTransitionName to 'none' removes the element from its own transition group. It becomes part of the parent .page-content transition instead, resulting in a smooth unified animation.

Apply to Both Handlers

Remember to add this fix to both the click event handler and the popstate handler (for browser back/forward navigation).