React <ViewTransition>: Smooth Animations Made Simple

React <ViewTransition>: Smooth Animations Made Simple

Discover React’s new ViewTransition component and how it leverages concurrent features to create smooth, browser-native animations. Learn enter/exit effects, shared element transitions, list filtering, and Suspense integration with practical examples and CSS customization tips.

Aurora Scharff

Aurora Scharff

September 17, 2025

View transitions are coming to React as a built-in component. Instead of complex animation libraries, you'll soon be able to create smooth, browser-native animations with minimal, declarative code.

Note: ViewTransition builds on React's concurrent features. If you're unfamiliar with useTransition and related concepts, check out React Concurrent Features: An Overview first to understand the foundation.

What is ViewTransition?

ViewTransition is an experimental React component that wraps the browser's View Transition API, automatically coordinating animations with React's rendering cycle. When wrapped around elements that change during a transition, it creates smooth animations between states.

The component works by creating snapshots of the old and new states, then animating between them using the browser's native View Transition API.

Here's the basic API:

import { unstable_ViewTransition as ViewTransition } from 'react';

<ViewTransition>
  <Component />
</ViewTransition>

The component activates automatically when elements inside it change during a React transition, following these patterns:

  • Enter: When the ViewTransition itself is added to the DOM
  • Exit: When the ViewTransition itself is removed from the DOM
  • Update: When content inside changes (like props or children)
  • Share: When named ViewTransitions coordinate between mounting/unmounting trees

For interactive demos and the latest API reference, see the React Labs post and the official docs; both include visual examples and are recommended alongside this article.

Enter and Exit Animations

A common use case is animating elements as they appear and disappear. When a ViewTransition is added or removed during a React transition, React automatically triggers enter and exit animations.

Here's a collapsible video component example:

import { useState, startTransition } from 'react';
import { unstable_ViewTransition as ViewTransition } from 'react';

function App() {
  const [showVideo, setShowVideo] = useState(false);

  return (
    <div>
      <button onClick={() => startTransition(() => setShowVideo(!showVideo))}>
        {showVideo ? '➖' : '➕'}
      </button>
      {showVideo && (
        <ViewTransition>
          <Video video={videos[0]} />
        </ViewTransition>
      )}
    </div>
  );
}

The startTransition wrapper is essential here. ViewTransition animations only activate when the state change occurs within a React transition (created by startTransition, useDeferredValue, or similar concurrent features). Without startTransition, the video would appear and disappear instantly. With it, you get a smooth fade transition that feels polished and intentional.

Shared Element Transitions

For more sophisticated animations, you can create shared element transitions that animate the same logical element between different locations or states. This requires giving ViewTransitions the same name prop.

Consider a video that transitions from thumbnail to fullscreen:

function VideoThumbnail({ video, onExpand }) {
  return (
    <ViewTransition name="video-player">
      <div onClick={onExpand}>
        <img src={video.thumbnail} alt={video.title} />
      </div>
    </ViewTransition>
  );
}

function VideoFullscreen({ video, onCollapse }) {
  return (
    <ViewTransition name="video-player">
      <div>
        <button onClick={onCollapse}>✕</button>
        <video src={video.url} controls />
      </div>
    </ViewTransition>
  );
}

function App() {
  const [isFullscreen, setIsFullscreen] = useState(false);

  return isFullscreen ? (
    <VideoFullscreen
      video={videos[0]}
      onCollapse={() => startTransition(() => setIsFullscreen(false))}
    />
  ) : (
    <VideoThumbnail
      video={videos[0]}
      onExpand={() => startTransition(() => setIsFullscreen(true))}
    />
  );
}

When transitioning between these states, React recognizes the matching names and creates a shared element transition. The video appears to smoothly morph from thumbnail size to fullscreen, maintaining visual continuity.

The name prop creates a connection between the unmounting and mounting ViewTransitions. This works even when they're in completely different component trees, as long as they have the same name and the state update occurs during the same React transition.

List Filtering

ViewTransition works beautifully with dynamic lists. Here's how to animate a searchable video list using useDeferredValue:

import { useState, useDeferredValue } from 'react';
import { unstable_ViewTransition as ViewTransition } from 'react';

function VideoList({ videos }) {
  const [searchTerm, setSearchTerm] = useState('');
  // Deferring the search term creates a transition between the immediate
  // input update and the deferred filter update, activating ViewTransition
  const deferredSearchTerm = useDeferredValue(searchTerm);

  const filteredVideos = videos.filter(video =>
    video.title.toLowerCase().includes(deferredSearchTerm.toLowerCase())
  );

  return (
    <div>
      <input
        placeholder="Search videos..."
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      {filteredVideos.map(video => (
        <ViewTransition key={video.id}>
          <VideoCard video={video} />
        </ViewTransition>
      ))}
    </div>
  );
}

Each video smoothly animates in or out as the filter changes, with useDeferredValue automatically creating the transition that activates the ViewTransition animations.

Working with Suspense

ViewTransition coordinates beautifully with Suspense boundaries, handling the transition from loading states to content. You can position ViewTransition in two ways for different effects:

1. Wrapping both the fallback and content (treating the transition as an update):

<ViewTransition>
  <Suspense fallback={<VideoSkeleton />}>
    <Video />
  </Suspense>
</ViewTransition>

This creates a smooth cross-fade from skeleton to content, treating them as different states of the same logical element.

2. Wrapping the fallback and content separately (treating them as distinct elements):

<Suspense fallback={
  <ViewTransition>
    <VideoSkeleton />
  </ViewTransition>
}>
  <ViewTransition>
    <Video />
  </ViewTransition>
</Suspense>

This treats them as separate elements, allowing for custom enter/exit animations as the VideoSkeleton exits and Video enters.

Customizing Animations

While ViewTransition provides sensible defaults, you can customize animations using CSS View Transition classes. Instead of the default cross-fade, you can specify different animations for different activation types:

<ViewTransition
  enter="slide-in"
  exit="slide-out"
  update="fade-slow"
>
  <Content />
</ViewTransition>

Then define these animations in your global CSS using view transition pseudo-selectors:

/* Custom animation for elements exiting with class 'slide-out' */
::view-transition-old(.slide-out) {
  animation: slide-out-left 300ms ease-in;
}

/* Custom animation for elements entering with class 'slide-in' */
::view-transition-new(.slide-in) {
  animation: slide-in-right 300ms ease-out;
}

/* Custom animation properties for the transition group with class 'fade-slow' */
::view-transition-group(.fade-slow) {
  animation-duration: 800ms;
}

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

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

This approach works for any scenario, including the Suspense enter/exit pattern:

<Suspense fallback={
  <ViewTransition exit="slide-down">
    <VideoSkeleton />
  </ViewTransition>
}>
  <ViewTransition enter="slide-up">
    <Video />
  </ViewTransition>
</Suspense>

With corresponding CSS:

::view-transition-old(.slide-down) {
  animation: slide-out-down 300ms ease-in;
}

::view-transition-new(.slide-up) {
  animation: slide-in-up 300ms ease-out;
}

@keyframes slide-out-down {
  to { transform: translateY(20px); opacity: 0; }
}

@keyframes slide-in-up {
  from { transform: translateY(20px); opacity: 0; }
}

The skeleton slides down and fades out while the video slides up and fades in, producing a layered, dynamic effect that feels deliberate. This pattern gives you precise control over timing and motion while preserving React's simple, declarative mental model.

Framework Integration

Many Suspense-enabled routers like Next.js will automatically trigger ViewTransition's default cross-fade when navigating between pages, since their navigation is already wrapped in transitions.

To try View Transitions in Next.js, add the following to your next.config.ts:

import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  experimental: {
    viewTransition: true
  }
};

export default nextConfig;

Conclusion

ViewTransition brings browser-native animations to React without the complexity of traditional animation libraries. By wrapping elements at the right boundaries and using React's concurrent features, you get performant transitions that feel smooth and intentional.

Start with simple enter/exit animations, then explore shared elements and list filtering as your needs grow. The React documentation includes many more examples and use cases beyond what's covered here. Since this is still experimental, test thoroughly before considering it for production applications.


Sources:

More certificates.dev articles

Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.

Looking for Certified Developers?

We can help you recruit Certified Developers for your organization or project. The team has helped many customers employ suitable resources from a pool of 100s of qualified Developers.

Let us help you get the resources you need.

Contact Us
Customer Testimonial for Hiring
like a breath of fresh air
Everett Owyoung
Everett Owyoung
Head of Talent for ThousandEyes
(a Cisco company)