Skip to main content

View Transitions API: Page Animations Without JavaScript Libraries

June 2, 2026

</>

For years, smooth page transitions required heavy JavaScript libraries: Framer Motion, GSAP, or custom animation frameworks. These solutions work, but they come with bundle cost, complexity, and the overhead of fighting the browser's default navigation model.

The View Transitions API changes this. Now baseline-supported in Chrome, Firefox, and Safari, it enables cross-fade and morphing animations between page states using a single CSS property and minimal JavaScript — with no library dependencies. For multi-page applications, Next.js 14+ integrates the API natively.


How View Transitions Work

The mechanism is elegant:

  1. You call document.startViewTransition(() => updateDOM()).
  2. The browser captures a screenshot of the current state.
  3. Your DOM update runs.
  4. The browser captures the new state.
  5. The browser animates between the two screenshots using CSS.
VIEW TRANSITION FLOW:

 Before State          Transition              After State
┌────────────┐         ┌──────────┐          ┌────────────┐
  Page A     capture │Old::view  animate    Page B    
            │────────►│New::view │─────────►│            
  Screenshot│         (crossfade│            Rendered  
└────────────┘          or morph)          └────────────┘
                       └──────────┘

Basic Page Transition

// Basic: wrap any DOM update in startViewTransition
async function navigateToPage(url) {
  if (!document.startViewTransition) {
    // Fallback for unsupported browsers
    window.location.href = url;
    return;
  }

  document.startViewTransition(async () => {
    const response = await fetch(url);
    const html = await response.text();
    const parser = new DOMParser();
    const newDoc = parser.parseFromString(html, 'text/html');

    // Swap only the main content
    document.querySelector('main').replaceWith(
      newDoc.querySelector('main')
    );

    // Update the URL
    history.pushState({}, '', url);
  });
}

By default, this gives you a cross-fade between the old and new page states. No CSS needed for the basic effect.


Customizing Transitions with CSS

The API exposes two pseudo-elements you can style:

/* ::view-transition-old — the screenshot of the page before */
/* ::view-transition-new — the screenshot of the page after  */

/* Default cross-fade (already happens without this) */
::view-transition-old(root) {
  animation: fade-out 0.25s ease-out;
}

::view-transition-new(root) {
  animation: fade-in 0.25s ease-in;
}

@keyframes fade-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}

@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

Slide Transition

/* Slide left on forward navigation */
::view-transition-old(root) {
  animation: slide-out-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

::view-transition-new(root) {
  animation: slide-in-right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

@keyframes slide-out-left {
  from { transform: translateX(0); }
  to   { transform: translateX(-100%); }
}

@keyframes slide-in-right {
  from { transform: translateX(100%); }
  to   { transform: translateX(0); }
}

/* Slide right on back navigation */
[data-direction="back"]::view-transition-old(root) {
  animation: slide-out-right 0.3s ease;
}

[data-direction="back"]::view-transition-new(root) {
  animation: slide-in-left 0.3s ease;
}

Element-Level Morphing with view-transition-name

The most powerful feature is element morphing: a specific element smoothly animates from its position in the old state to its new position. This creates the "shared element transition" effect that mobile apps have used for years.

/* Give the hero image a unique transition name */
.hero-image {
  view-transition-name: hero-image;
}

/* Give the article title a transition name */
.article-title {
  view-transition-name: article-title;
}
// When navigating from a card to a detail page,
// the card image and title will morph into position
document.startViewTransition(async () => {
  await loadDetailPage(articleId);
});
// The browser automatically animates elements with matching
// view-transition-name values between the two states.

The CSS to control the morph:

/* The morphing element gets its own pair of pseudo-elements */
::view-transition-old(hero-image) {
  animation: none; /* Let the browser handle morphing automatically */
}

::view-transition-new(hero-image) {
  animation: none;
}

/* Or customize the morph: */
::view-transition-old(article-title) {
  animation: 0.4s ease-in-out both title-shrink;
}

::view-transition-new(article-title) {
  animation: 0.4s ease-in-out both title-grow;
}

Next.js Integration

Next.js App Router supports View Transitions natively via the unstable_ViewTransition component (stabilized in Next.js 15+):

// app/blog/page.tsx
import { unstable_ViewTransition as ViewTransition } from 'next';
import Link from 'next/link';

export default function BlogList({ posts }) {
  return (
    <ul>
      {posts.map(post => (
        <li key={post.slug}>
          <Link href={`/blog/${post.slug}`}>
            <ViewTransition name={`post-image-${post.slug}`}>
              <img src={post.thumbnail} alt={post.title} />
            </ViewTransition>
            <ViewTransition name={`post-title-${post.slug}`}>
              <h2>{post.title}</h2>
            </ViewTransition>
          </Link>
        </li>
      ))}
    </ul>
  );
}

// app/blog/[slug]/page.tsx
export default function BlogPost({ post }) {
  return (
    <article>
      <ViewTransition name={`post-image-${post.slug}`}>
        <img src={post.thumbnail} alt={post.title} className="hero-image" />
      </ViewTransition>
      <ViewTransition name={`post-title-${post.slug}`}>
        <h1>{post.title}</h1>
      </ViewTransition>
      <div>{post.content}</div>
    </article>
  );
}

The image and title will smoothly morph from their list position to their detail page position on navigation.


Respecting User Preferences

Always disable transitions for users who prefer reduced motion:

/* Disable all transitions for reduced-motion preference */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Browser Support Check

// Progressive enhancement — works without the API too
export function withViewTransition(updateFn: () => void | Promise<void>) {
  if (!('startViewTransition' in document)) {
    return updateFn(); // Fallback: instant update
  }
  return document.startViewTransition(updateFn);
}

Conclusion

The View Transitions API delivers the kind of fluid, app-like page transitions that previously required significant JavaScript tooling — now as a browser primitive with no bundle cost. The combination of full-page cross-fades, directional slide transitions, and element-level morphing covers virtually every transition pattern used in modern web applications. With baseline browser support established in 2025 and Next.js native integration, there is no longer a good reason to reach for a JavaScript animation library for page transitions.

Recommended Posts