1 min 9 sec read

Page Transitions in Next.js with React's ViewTransition Component

Learn how to create fluid, app-like page transitions in your Next.js application using React's new experimental ViewTransition component. We'll walk through a practical blog example, transforming a list item into a full post page with a slick morphing animation.

logo
Maurya Patel

What is ViewTransition?

ViewTransition is an experimental React component that utilizes the browser’s View Transition API to deliver smooth animations between changing UI states. It automatically coordinates these transitions with React’s rendering cycle by capturing snapshots of the old and new states, then animating between them using the browser’s native transition system.

Default Implementation looks like this:

import ‍​{ ‍​ViewTransition ‍​} ‍​from ‍​'react';

<ViewTransition>
 ‍​ ‍​<Component ‍​/>
</ViewTransition>

Canary

The <ViewTransition /> API is currently only available in React’s Canary and Experimental channels.

Shared Element Transitions

Shared Element Transitions make it possible for elements that appear in both the old and new views to animate smoothly between states, creating a continuous visual flow. When using ViewTransition, elements that share the same identifier (for example, using the view-transition-name attribute) are automatically recognized as the same element across transitions.

Here’s a simple example of how this works:

For our example we are implement this on blog posts so we have to assign a Starting point and Destination point for our example our starting point is PostCard.tsx and Destination is Post.tsx

PostCard.tsx
1import ‍​React, ‍​{ ‍​FC, ‍​ViewTransition ‍​} ‍​from ‍​"react";
2import ‍​Link ‍​from ‍​"next/link";
3import ‍​{ ‍​Post ‍​} ‍​from ‍​"@/app/types";
4// ... other imports
5
6interface ‍​PostCardProps ‍​{
7 ‍​ ‍​ ‍​ ‍​post: ‍​Post;
8}
9
10const ‍​PostCard: ‍​FC<PostCardProps> ‍​= ‍​({ ‍​post ‍​}) ‍​=> ‍​{
11 ‍​ ‍​ ‍​ ‍​return ‍​(
12 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​// The entire card has a transition name
13 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<ViewTransition ‍​name={`post-${post.id}`}>
14 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<Link ‍​href={`/${post.id}`} ‍​className="group ‍​block ‍​...">
15
16 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​{/* The title has a unique name */}
17 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<ViewTransition ‍​name={`post-${post.title}`}>
18 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<h2 ‍​className="text-xl ‍​...">
19 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​{post.title}
20 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</h2>
21 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</ViewTransition>
22
23 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​{/* The body has a unique name */}
24 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<ViewTransition ‍​name={`post-${post.body}`}>
25 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<p ‍​className="text-zinc-600 ‍​...">
26 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​{post.body}
27 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</p>
28 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</ViewTransition>
29
30 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​{/* The tags container also has a name */}
31 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<ViewTransition ‍​name={`post-tags-${post.id}`}>
32 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​{post.tags.length ‍​> ‍​0 ‍​&& ‍​(
33 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<div ‍​className="flex ‍​...">
34 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​{post.tags.map((tag) ‍​=> ‍​(
35 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<span ‍​key={tag} ‍​className="text-xs ‍​...">
36 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​{/* Even individual tags can be transitioned */}
37 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<ViewTransition ‍​name={`post-tags-${post.id}-${tag}`}>
38 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​#{tag}
39 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</ViewTransition>
40 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</span>
41 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​))}
42 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</div>
43 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​)}
44 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</ViewTransition>
45 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​
46 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​{/* ... other card elements ... */}
47
48 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</Link>
49 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</ViewTransition ‍​>
50 ‍​ ‍​ ‍​ ‍​);
51};
52
53export ‍​default ‍​PostCard;

Post.tsx
1import ‍​{ ‍​Post ‍​as ‍​PostType ‍​} ‍​from ‍​'@/app/types'
2import ‍​React, ‍​{ ‍​FC, ‍​ViewTransition ‍​} ‍​from ‍​'react'
3// ... other imports
4
5interface ‍​PostProps ‍​{
6 ‍​ ‍​post: ‍​PostType
7}
8
9const ‍​Post: ‍​FC<PostProps> ‍​= ‍​({ ‍​post ‍​}) ‍​=> ‍​{
10 ‍​ ‍​return ‍​(
11 ‍​ ‍​ ‍​ ‍​// The name `post-${post.id}` matches the PostCard's wrapper
12 ‍​ ‍​ ‍​ ‍​<ViewTransition ‍​name={`post-${post.id}`}>
13 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<article ‍​className="max-w-3xl ‍​...">
14 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<header ‍​className="mb-8 ‍​...">
15
16 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​{/* This name `post-${post.title}` matches the PostCard's <h2> */}
17 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<ViewTransition ‍​name={`post-${post.title}`}>
18 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<h1 ‍​className="text-3xl ‍​...">
19 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​{post.title}
20 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</h1>
21 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</ViewTransition>
22 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​
23 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​{/* ... metadata ... */}
24 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</header>
25
26 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​{/* This name `post-${post.body}` matches the PostCard's <p> */}
27 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<ViewTransition ‍​name={`post-${post.body}`}>
28 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<section ‍​className="prose ‍​...">
29 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<p ‍​className="text-lg ‍​...">
30 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​{post.body}
31 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</p>
32 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</section>
33 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</ViewTransition>
34
35 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​{/* This name `post-tags-${post.id}` matches the PostCard's tag container */}
36 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<ViewTransition ‍​name={`post-tags-${post.id}`}>
37 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​{post.tags ‍​&& ‍​(
38 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<div ‍​className="mt-10 ‍​...">
39 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​{post.tags.map((tag, ‍​i) ‍​=> ‍​(
40 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<span ‍​key={i} ‍​className="px-3 ‍​...">
41 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​
42 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​{/* And this name `post-tags-${post.id}-${tag}` matches the individual tag */}
43 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<ViewTransition ‍​name={`post-tags-${post.id}-${tag}`}>
44 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​#{tag}
45 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</ViewTransition>
46 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</span>
47 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​))}
48 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</div>
49 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​)}
50 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</ViewTransition>
51 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</article>
52 ‍​ ‍​ ‍​ ‍​</ViewTransition>
53 ‍​ ‍​)
54}
55
56export ‍​default ‍​Post

Conclusion

That’s all there is to it. By introducing a single <ViewTransition> component and maintaining a consistent name prop, we’ve transformed a typical Next.js SPA into an experience that feels as smooth and polished as a native app. This declarative animation approach harnesses the power of the browser’s View Transition API, giving React developers an elegant, modern way to create more immersive and visually engaging user interfaces.

Check out Code GitHub: Code

Live demo : Demo

Enjoyed this article?

Share it with your network or friends.