DEV Community

Omoshola E.
Omoshola E.

Posted on

The Complete Guide to React ViewTransition (Just Released!)

๐ŸŽ‰ Big news! The React team just released ViewTransition - a game-changing feature for creating smooth, professional animations in React!

Ever noticed how some websites feel buttery smooth when content changes, while others feel jumpy and jarring? The secret is often in the animations. Today, we're diving into React's brand new ViewTransition feature that makes your apps feel premium with surprisingly little code.

What is ViewTransition?

Imagine you're updating content on your website. Normally, old content vanishes and new content appears instantlyโ€”like flipping a light switch. ViewTransition is React's way of making those changes smooth and animated instead of jarring and instant.

Think of it like this:

  • Without ViewTransition: When you update something on the page (like switching between two pieces of content), it just instantly swaps - old content disappears, new content appears.
  • With ViewTransition: The same update happens, but now it's animated, maybe the old content fades out while the new content fades in, or slides in from the side.

Real-world example:
Imagine you have a tab component. When you click from "Tab A" to "Tab B":

Regular way: Tab A content vanishes, Tab B content appears instantly
With <ViewTransition>: Tab A content smoothly fades/slides out as Tab B content fades/slides in

So is basically saying: "Hey, animate the changes that happen inside me instead of making them instant."


The Basic Rules

For ViewTransition to work, you need three key ingredients:

  1. <ViewTransition> must be the FIRST thing your component returns (before any other DOM elements)
  2. Wrap your content in <ViewTransition>
  3. Update the content inside startTransition()

Rule #1: ViewTransition Must Be First! โš ๏ธ

This is critical: <ViewTransition> must be the very first element your component returns. If there are any other HTML elements before it, animations won't work.

Think of it like a wrapper around your entire component - it needs to be the outermost layer for React to detect when the component appears or disappears.

// โŒ WRONG - <div> comes before ViewTransition
function BadNotification() {
  return (
    <div className="container">
      <h2>Notifications</h2>
      <ViewTransition>
        <div className="notification">
          You have a new message!
        </div>
      </ViewTransition>
    </div>
  );
}

// โŒ WRONG - <Fragment> with <h1> before ViewTransition
function BadProfile() {
  return (
    <>
      <h1>User Profile</h1>
      <ViewTransition>
        <div className="profile-card">
          Profile content...
        </div>
      </ViewTransition>
    </>
  );
}

// โœ… CORRECT - ViewTransition is the FIRST thing returned
function GoodNotification() {
  return (
    <ViewTransition>
      <div className="container">
        <h2>Notifications</h2>
        <div className="notification">
          You have a new message!
        </div>
      </div>
    </ViewTransition>
  );
}

// โœ… CORRECT - ViewTransition wraps everything
function GoodProfile() {
  return (
    <ViewTransition>
      <h1>User Profile</h1>
      <div className="profile-card">
        Profile content...
      </div>
    </ViewTransition>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why this matters: React looks for <ViewTransition> at the component's root to trigger enter/exit animations. If it finds other elements first, it treats your component as a regular update, not an animated transition.


Rule #2: Use startTransition for Updates

Regular setState updates happen instantly. To animate them, wrap the update in startTransition():

// โŒ This won't animate - just regular setState
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <ViewTransition>
      <div>{count}</div>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </ViewTransition>
  );
}

// โœ… This will animate - wrapped in startTransition
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <ViewTransition>
      <div>{count}</div>
      <button onClick={() => startTransition(() => setCount(count + 1))}>
        Increment
      </button>
    </ViewTransition>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why? startTransition tells React "this is a non-urgent update that can be animated." Regular setState is considered urgent and happens instantly without animation.


The Four Types of Animations

ViewTransition gives you four moments where animations can happen:

1. ๐Ÿ‘‹ Enter - When Something Appears

This happens when the <ViewTransition> wrapper itself didn't exist before and now it does. If the component get mounted.

function Notification() {
  return (
    <ViewTransition class="fade-in">
      <div className="notification">Message sent!</div>
    </ViewTransition>
  );
}

function App() {
  const [show, setShow] = useState(false);

  const notify = () => {
    startTransition(() => {
      setShow(true);
    });
  };

  return (
    <div>
      <button onClick={notify}>Send</button>
      {show && <Notification />}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

What you see: The notification smoothly fades in when you click "Send."


2. ๐Ÿ‘‹ Exit - When Something Disappears

This happens when the <ViewTransition> wrapper gets removed. If the component unmounts...

function Modal({ onClose }) {
  return (
    <ViewTransition class="fade-out">
      <div className="modal">
        <h2>Modal Content</h2>
        <button onClick={() => startTransition(onClose)}>
          Close
        </button>
      </div>
    </ViewTransition>
  );
}

function App() {
  const [showModal, setShowModal] = useState(false);

  return (
    <div>
      <button onClick={() => startTransition(() => setShowModal(true))}>
        Open Modal
      </button>
      {showModal && <Modal onClose={() => setShowModal(false)} />}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

What you see: The modal smoothly fades out when you click "Close."


3. ๐Ÿ”„ Update - When Something Changes

This happens when content inside the <ViewTransition> changes, or when the wrapper itself moves or resizes.

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <ViewTransition class="pulse">
      <div className="counter">{count}</div>
      <button onClick={() => startTransition(() => setCount(count + 1))}>
        Increment
      </button>
    </ViewTransition>
  );
}
Enter fullscreen mode Exit fullscreen mode

What you see: The number pulses each time it changes.


4. โœจ Share - When Something Morphs Between Locations

This is the coolest one! It happens when you delete (unmount) a named ViewTransition and add (mount) another one with the same name. React treats them as the same element transforming.

function App() {
  const [view, setView] = useState('grid');
  const [selectedId, setSelectedId] = useState(null);

  const selectImage = (id) => {
    startTransition(() => {
      setSelectedId(id);
      setView('detail');
    });
  };

  const goBack = () => {
    startTransition(() => {
      setView('grid');
    });
  };

  if (view === 'grid') {
    return (
      <div className="grid">
        {images.map(img => (
          <ViewTransition key={img.id} name={`image-${img.id}`}>
            <img 
              src={img.thumbnail} 
              onClick={() => selectImage(img.id)}
              className="thumbnail"
            />
          </ViewTransition>
        ))}
      </div>
    );
  }

  return (
    <div>
      <button onClick={goBack}>Back</button>
      <ViewTransition name={`image-${selectedId}`}>
        <img 
          src={images[selectedId].fullSize}
          className="full-size"
        />
      </ViewTransition>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

What you see: Click a thumbnail and it smoothly expands into a full-size image. Click back and it shrinks back to the grid.

The magic: Both ViewTransitions have the same name prop, so React morphs between them instead of fading out and in.


View Transition Class

The View Transition Class, is css class name that React adds to your elements during the animation, so you can style how they animate. It can be a string or an object.
By default, ViewTransition uses a simple crossfade.

Two Ways to Use It

  1. String - One class for everything
<ViewTransition class="my-animation">
  <div>Content</div>
</ViewTransition>
Enter fullscreen mode Exit fullscreen mode
.my-animation {
  animation: slideIn 0.3s ease;
}

@keyframes slideIn {
  from {
    transform: translateX(-100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}
Enter fullscreen mode Exit fullscreen mode

What happens: React adds the class my-animation to the element during any animation (enter, exit, update, or share).

  1. Object - Different classes for different animation types
<ViewTransition 
  class={{
    enter: "slide-in",
    exit: "slide-out",
    update: "pulse",
    share: "morph"
  }}
>
  <div>Content</div>
</ViewTransition>
Enter fullscreen mode Exit fullscreen mode
.slide-in {
  animation: slideFromRight 0.3s ease;
}

.slide-out {
  animation: slideToLeft 0.3s ease;
}

.pulse {
  animation: scaleUp 0.2s ease;
}

.morph {
  animation: smoothTransform 0.4s ease;
}
Enter fullscreen mode Exit fullscreen mode

What happens:

  1. When element appears โ†’ adds class slide-in
  2. When element disappears โ†’ adds class slide-out
  3. When element changes โ†’ adds class pulse
  4. When element morphs โ†’ adds class morph

Turn Off Specific Animations

<ViewTransition 
  class={{
    enter: "fade-in",
    exit: "none", // No animation when leaving
    update: "pulse"
  }}
>
  <div>Content</div>
</ViewTransition>
Enter fullscreen mode Exit fullscreen mode

๐ŸŽฏ Callbacks: Do Something After Animation

Sometimes you want to run code after an animation finishes:

<ViewTransition 
  onEnter={(element) => {
    console.log("Appeared!");
    element.focus(); // Focus on the new element
  }}
  onExit={(element) => {
    console.log("Disappeared!");
  }}
  onUpdate={(element) => {
    console.log("Changed!");
  }}
>
  <div>Content</div>
</ViewTransition>
Enter fullscreen mode Exit fullscreen mode

Real-world example: Auto-dismiss notifications

<ViewTransition 
  onEnter={(element) => {
    // After the notification fades in, start a timer
    setTimeout(() => {
      startTransition(() => setShow(false));
    }, 3000);
  }}
>
  <div className="notification">Saved successfully!</div>
</ViewTransition>
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“‹ Animating Lists

The View Transition API provides smooth, animated reordering of lists without manual animation code. When list items change position, the browser automatically creates cross-fade animations that make items appear to glide to their new positions.

This makes list sorting, filtering, and reordering feel much more polished and professional with minimal code!

function TodoList() {
  const [items, setItems] = useState([
    { id: 1, text: 'Buy milk' },
    { id: 2, text: 'Walk dog' },
    { id: 3, text: 'Read book' }
  ]);

  const shuffle = () => {
    startTransition(() => {
      setItems([...items].sort(() => Math.random() - 0.5));
    });
  };

  return (
    <div>
      <button onClick={shuffle}>Shuffle</button>
      {items.map(item => (
        <TodoItem key={item.id} item={item} />
      ))}
    </div>
  );
}

// โœ… ViewTransition at the top - will animate!
function TodoItem({ item }) {
  return (
    <ViewTransition class="reorder">
      <div className="todo-item">
        {item.text}
      </div>
    </ViewTransition>
  );
}
Enter fullscreen mode Exit fullscreen mode

What you see: Click "Shuffle" and items smoothly slide to their new positions.

Important: ViewTransition must be the first thing returned by the list component. This won't work:

// โŒ Won't animate - ViewTransition is wrapped
function TodoItem({ item }) {
  return (
    <div className="todo-item">
      <ViewTransition>
        <span>{item.text}</span>
      </ViewTransition>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

โณ Working with Suspense

ViewTransition plays nicely with Suspense for loading states. There are two ways to do it:

Method 1: Crossfade (Update Animation)

Wrap the entire Suspense with ViewTransition:

<ViewTransition class="crossfade">
  <Suspense fallback={<LoadingSpinner />}>
    <HeavyContent />
  </Suspense>
</ViewTransition>
Enter fullscreen mode Exit fullscreen mode

What happens:

  1. Shows loading spinner
  2. Content loads
  3. Smoothly crossfades from spinner to content

Visual: Both fade together, blending smoothly.


Method 2: Separate Enter/Exit Animations

Wrap the fallback AND content separately:

<Suspense 
  fallback={
    <ViewTransition class="spinner-exit">
      <LoadingSpinner />
    </ViewTransition>
  }
>
  <ViewTransition class="content-enter">
    <HeavyContent />
  </ViewTransition>
</Suspense>
Enter fullscreen mode Exit fullscreen mode

What happens:

  1. Spinner enters (fades in)
  2. Content loads
  3. Spinner exits (fades out)
  4. Content enters (slides in)

Visual: Distinct stagesโ€”spinner leaves, then content arrives.


React Waits for Everything to Be Ready

Before animating, React intelligently waits for:

  • โœ… Data to load
  • โœ… CSS files to load
  • โœ… Fonts to load (up to 500ms)
  • โœ… Images inside ViewTransition to load

Why? To avoid flickering! You don't want to see text appear then suddenly change font, or layout shift when images pop in.


๐Ÿ›ฃ๏ธ Building Routers with ViewTransition

If you're building a router, there are two important things to know:

1. Unblock Navigation in useLayoutEffect

React waits for navigation to complete before animating (so scroll position can be restored correctly). Your router must unblock in useLayoutEffect, not useEffect:

// โŒ WRONG - Causes deadlock
function Router() {
  const [blocked, setBlocked] = useState(true);

  useEffect(() => {
    setBlocked(false); // Too late!
  }, []);

  return blocked ? <Loading /> : <Page />;
}

// โœ… CORRECT
function Router() {
  const [blocked, setBlocked] = useState(true);

  useLayoutEffect(() => {
    setBlocked(false); // Just in time!
  }, []);

  return blocked ? <Loading /> : <Page />;
}
Enter fullscreen mode Exit fullscreen mode

Why? useLayoutEffect runs before the browser paints, so React can continue. useEffect runs after, causing a deadlock.


2. Back Button Won't Animate (Yet)

Currently, the browser back button doesn't trigger animations. This is because the old popstate event must be synchronous (to restore scroll/form data immediately), but animations take time.

Solution: Upgrade to the Navigation API (modern browsers):

// Check if supported
if ('navigation' in window) {
  navigation.addEventListener('navigate', (event) => {
    if (event.canIntercept) {
      event.intercept({
        async handler() {
          // Can be async - allows animations!
          await startTransition(() => {
            updateRoute(event.destination.url);
          });
        }
      });
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

With the Navigation API, back button animations work! ๐ŸŽ‰


โš ๏ธ Important Rules to always keep in mind when working with ViewTransition

1. ViewTransition Must Be First

// โŒ Won't work - there's a div before ViewTransition
function Bad() {
  return (
    <div>
      <h1>Title</h1>
      <ViewTransition>
        <div>Content</div>
      </ViewTransition>
    </div>
  );
}

// โœ… Works - ViewTransition is first
function Good() {
  return (
    <ViewTransition>
      <div>
        <h1>Title</h1>
        <div>Content</div>
      </div>
    </ViewTransition>
  );
}
Enter fullscreen mode Exit fullscreen mode

2. It Animates as a Group, Not Individual Pieces

ViewTransition takes a "snapshot" of everything inside and animates that snapshot. Individual elements don't move separately.

<ViewTransition>
  <div>
    <h1>Title</h1>
    <p>Paragraph 1</p>
    <p>Paragraph 2</p>
  </div>
</ViewTransition>
Enter fullscreen mode Exit fullscreen mode

What happens: Everything moves together as one unit.

If you want individual animations:

<div>
  <ViewTransition><h1>Title</h1></ViewTransition>
  <ViewTransition><p>Paragraph 1</p></ViewTransition>
  <ViewTransition><p>Paragraph 2</p></ViewTransition>
</div>
Enter fullscreen mode Exit fullscreen mode

Now each piece can animate independently!


3. Shared Names Must Be Unique

For "share" animations (morphing), each name must be unique at any given time:

// โŒ BAD - Two elements with same name exist simultaneously
<div>
  <ViewTransition name="avatar">
    <img src="user1.jpg" />
  </ViewTransition>
  <ViewTransition name="avatar">
    <img src="user2.jpg" />
  </ViewTransition>
</div>

// โœ… GOOD - Only one exists at a time
{currentUser === 'user1' && (
  <ViewTransition name="avatar">
    <img src="user1.jpg" />
  </ViewTransition>
)}
{currentUser === 'user2' && (
  <ViewTransition name="avatar">
    <img src="user2.jpg" />
  </ViewTransition>
)}
Enter fullscreen mode Exit fullscreen mode

4. Both Elements Must Be Visible for Share Animation

If you're morphing between two elements (share animation) and one is scrolled off-screen, React won't morph them. Instead, they'll fade out/in separately.

Why? To prevent weird effects where images fly across your entire screen from way down below.


5. โ™ฟ Accessibility: Respect User Preferences

Some users get motion sickness from animations. Always add this CSS:

.my-animation {
  animation: slideIn 0.3s ease;
}

/* Disable for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
  .my-animation {
    animation: none;
  }
}
Enter fullscreen mode Exit fullscreen mode

React doesn't do this automaticallyโ€”you must add it yourself!


6. ๐Ÿšซ NEVER Use localStorage or sessionStorage

Critical: Browser storage APIs are not supported in React artifacts and will cause failures.

// โŒ NEVER DO THIS - Will fail!
function BadComponent() {
  const [data, setData] = useState(
    JSON.parse(localStorage.getItem('data'))
  );

  return <ViewTransition>...</ViewTransition>;
}

// โœ… Use React state instead
function GoodComponent() {
  const [data, setData] = useState(initialData);

  return <ViewTransition>...</ViewTransition>;
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿš€ Real-World Example: Complete App

Let's put it all together with a simple image gallery:

import { useState, startTransition, Suspense } from 'react';

function ImageGallery() {
  const [view, setView] = useState('grid');
  const [selectedId, setSelectedId] = useState(null);

  const images = [
    { id: 1, thumb: 'thumb1.jpg', full: 'full1.jpg', title: 'Sunset' },
    { id: 2, thumb: 'thumb2.jpg', full: 'full2.jpg', title: 'Mountain' },
    { id: 3, thumb: 'thumb3.jpg', full: 'full3.jpg', title: 'Ocean' }
  ];

  const selectImage = (id) => {
    startTransition(() => {
      setSelectedId(id);
      setView('detail');
    });
  };

  const goBack = () => {
    startTransition(() => {
      setView('grid');
    });
  };

  if (view === 'grid') {
    return (
      <div className="grid">
        {images.map(img => (
          <ViewTransition key={img.id} name={`image-${img.id}`}>
            <img 
              src={img.thumb}
              alt={img.title}
              onClick={() => selectImage(img.id)}
              className="thumbnail"
            />
          </ViewTransition>
        ))}
      </div>
    );
  }

  const selected = images.find(img => img.id === selectedId);

  return (
    <div className="detail-view">
      <button onClick={goBack}>โ† Back</button>

      <Suspense 
        fallback={
          <ViewTransition class="skeleton-animation">
            <div className="skeleton" />
          </ViewTransition>
        }
      >
        <ViewTransition name={`image-${selectedId}`}>
          <img 
            src={selected.full}
            alt={selected.title}
            className="full-image"
          />
        </ViewTransition>
      </Suspense>

      <h2>{selected.title}</h2>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

CSS:

.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

.thumbnail {
  width: 100%;
  cursor: pointer;
  border-radius: 8px;
  transition: transform 0.2s;
}

.thumbnail:hover {
  transform: scale(1.05);
}

.detail-view {
  max-width: 800px;
  margin: 0 auto;
}

.full-image {
  width: 100%;
  border-radius: 12px;
}

.skeleton {
  width: 100%;
  height: 500px;
  background: linear-gradient(
    90deg,
    #f0f0f0 25%,
    #e0e0e0 50%,
    #f0f0f0 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: 12px;
}

@keyframes shimmer {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}

/* Respect user preferences */
@media (prefers-reduced-motion: reduce) {
  .thumbnail {
    transition: none;
  }

  .skeleton {
    animation: none;
  }
}
Enter fullscreen mode Exit fullscreen mode

What this app does:

  1. Shows a grid of thumbnail images
  2. Click a thumbnail โ†’ It smoothly expands to full size (share animation)
  3. Shows a skeleton loader while the full image loads (suspense)
  4. Click back โ†’ Image smoothly shrinks back to grid position

๐Ÿ“ Key Takeaways

  1. Always wrap updates in startTransition() - Regular setState won't animate
  2. Put <ViewTransition> first - It must be the first thing your component returns
  3. Use name prop for morphing - Same name = elements morph between positions
  4. Customize with class prop - Different animations for enter/exit/update/share
  5. Respect accessibility - Add prefers-reduced-motion media queries
  6. Works with Suspense - Two methods: crossfade or separate enter/exit
  7. Never use browser storage - Use React state instead
  8. Router builders - Use useLayoutEffect and Navigation API for best results

๐ŸŒ Browser Support

ViewTransition currently works in:

  • โœ… Modern browsers with View Transitions API support
  • โœ… Web applications (React DOM)
  • โŒ React Native (coming in the future)

๐ŸŽฌ Conclusion

ViewTransition is React's brand new powerful tool for creating smooth, professional animations with minimal code. Now that it's officially released, you can start using it in your projects today!

Instead of managing complex animation libraries, you simply:

  1. Wrap content in <ViewTransition>
  2. Update inside startTransition()
  3. Style with CSS

That's it! React handles the restโ€”snapshots, timing, coordination, and cleanup.

Start with simple enter/exit animations, then level up to shared element transitions for that premium app feel. Your users will love the polish! โœจ


This is a brand new feature - what animations are you excited to build with ViewTransition? Drop a comment below! ๐Ÿ‘‡

If you found this helpful, give it a โค๏ธ and share with your fellow developers!

Happy coding! ๐Ÿš€

Top comments (0)