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.
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
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;
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.