With the View Transitions API having more widespread support across browsers, and React adding an experimental flag (as of 23rd April 2025) and component for the feature, I decided to play around with setting up transitions. This can provide those lovely hero image fades, and more, so we'll quickly talk about how you can set up using the View Transitions component and setting it up for your own animations.
Enabling Transitions in Next.js
If you're using Next.js, you'll need to enable View Transitions with the experimental flag, you can do this in our config file.
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
viewTransition: true,
},
}
module.exports = nextConfig
Structuring our code
To get view transitions working, we need to get some base code set up. I've put together a few example components that show how you could structure your site, whilst taking into account the fact you may want to use loops and unknown amounts of elements.
The below code presumes you are running the following:
- Next.js v15.3+
- Using the app router for folder structure.
Layout (app/layout.tsx)
import { unstable_ViewTransition as ViewTransition } from "react";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ViewTransition>{children}</ViewTransition>
</body>
</html>
);
}
Home Page (app/page.tsx)
import ArticleCard from "@/components/article-card";
export default function Home() {
return (
<div className="p-10 grid grid-cols-3 gap-10">
<ArticleCard id={1} />
<ArticleCard id={2} />
<ArticleCard id={3} />
<ArticleCard id={4} />
....
<ArticleCard id={210} />
</div>
);
}
Card Component (components/article-card.tsx)
import Image from "next/image";
import Link from "next/link";
export default function ArticleCard({ id }: { id: number }) {
const imageStyle = { viewTransitionName: `article-image-${index}` };
return (
<Link
href={`/article/${id}`}
className="article-card block bg-gray-100 mb-4"
>
<Image
src="/image.jpg"
width="600"
height="300"
alt="Image of a beautiful dog"
className="img w-full block"
style={imageStyle}
/>
<h3 className="p-4">
Example Blog Title {id}
</h3>
</Link>
);
}
Single Article Page (app/article/[id]/page.tsx)
import Image from "next/image";
import Link from "next/link";
export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const imageStyle = { viewTransitionName: `article-image-${id}` };
return (
<article className="p-10 bg-red-100 h-screen">
<Image
src="/image.jpg"
width="1200"
height="900"
alt="Image of a beautiful dog"
style={imageStyle}
/>
<h1 className="text-3xl" style={headingStyle}>
Example Blog Title {id}
</h1>
<Link className="mb-4 block" href="/">
Go Back
</Link>
</article>
);
}
The structure of our example is a list of articles, each with a unique ID. These then link to a single page with a URL parameter of the same ID. Within the code, there are a few key parts to take away about why we've set it up the way we have, and what's important:
- In the article-card component, we are setting a
view-transition-name
property on the image. This is so the Transition API can identify what is going to transition into what. As if we are using an unknown number of elements, we are setting a unique identifier based off theID
of the article. - In the article page, we're also setting a
view-transition-name
however this time, we're using the query param to get the ID so that it matches up with our article-card. - If you were to add hundreds of cards, each one would animate to it's corresponding image thanks to the dynamic
view-transition-name
. Meaning, once set up, you don't need to add a new one to your CSS file for each new item.
Getting Fancy (with CSS)
If you want to take your transitions one step further, you can set up styles on how you want the browser to animate the content.
Changing a single animation
You can target a single animation, such as the 10th article image, by targeting the specific transition name, e.g. article-image-10
.
::view-transition-old(article-image-10) {
animation: 300ms ease-out fade-out;
}
::view-transition-new(article-image-10) {
animation: 300ms ease-in fade-in;
}
Changing the animation for all elements with one class
If you want to target everything in our loop, we can target this by setting a view transition class and then targeting the elements on that.
.article-card img {
view-transition-class: article-image;
}
article img {
view-transition-class: article-image;
}
@keyframes animate-in {
0% {
opacity: 0
}
100% {
opacity: 1
}
}
::view-transition-group(*.article-image) {
animation: animate-in ease-in 300ms;
}
In the above code, we're utilising the view-transition-group
property so we can target all elements with a view-transition-class
of article-image
. Rather than writing a few lines of CSS for every image, we tackle them all at once, keeping our CSS code nice and neat. Although the above only animates opacity, I'm sure you can get much more imaginative.
Note: although we write the
view-transition-class
here, we still need to ensure we keep theview-transition-name
on each object. This is so the browser knows how to identify them.