Photo by Indira Tjokorda on Unsplash
Page Transitions In ReactJS With React Router V6 and the Built-In View Transitions API (No Third-Party Libraries)
We will go step by step in a new tutorial without using any third-party libraries, we will only use a build-in JavaScript API: View Transitions API.
What is the View Transitions API
View Transitions API provides a way to create an animated transition between two documents (Views), without creating an overlap in the transition.
View Transitions make this process easy and strait-full , by allowing you to make your DOM change without any overlap between states, by creating a transition animation between the states using snap-shotted views.
The current implementation of this API targets single page applications (SPAs), In this tutorial we will explain how to do that using ReactJS.
View Transitions API browser support?
The View transition API should be on by default for Chrome 111 beta users.
You can enable it by accessing the experiment flag: chrome://flags/#view-transition in Chrome 109 and above.
The same process for Chromium based browsers (Microsoft Edge, Brave, Opera …).
For Mozilla Firefox, The View transitions API is not implemented yet.
Before going with this tutorial
We have talked about View Transitions API before (In this tutorial).
We explained what this API is, how to enable it in supported browsers and how to use it, a step by step guide, with a final working example application (MPA: multi page application), with full source code, all using only Vanilla JavaScript.
You must carefully read the CSS implementation, because we will go on with the same logic in this tutorial.
But To apply that to a ReactJS application we have to do that with a different approach.
What we are building
This is an animated GIF, showing our final application in action
Let’s code
Let’s see what we are building
In our Application, we will use create-react-app to create a starting project, that let’s as navigate between three pages or views: home, download and about.
First
npx create-react-app reactjs-react-router-view-transitions-api
&& cd reactjs-react-router-view-transitions-api
Second
npm install react-router-dom
To install React Router V6 package.
Our application will contain these files:
ReactJs - React Router - View Transitions API
└─ src
└─ img
├─ home.png
├─ about.png
└─ download.png
├─ About.js
├─ App.js
├─ Download.js
├─ Header.js
├─ Home.js
├─ PageNotFound.js
├─ Index.js
├─ styles.css
└─ ...
- The “/img” folder
contain the images used in our project.
- Index.js
It’s the entry point of our project
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import App from "./App";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<StrictMode>
<BrowserRouter>
<Routes>
<Route path="/*" element={<App />} />
</Routes>
</BrowserRouter>
</StrictMode>
);
As you can see, we are routing all pages to the <App/> component, using the “/*” in the path prop.
In React Router v6, All routes : <Route> must be contained in the <BrowserRouter> and <Routes>
<BrowserRouter>
<Routes>
<Route path="/*" element={<App />} />
</Routes>
</BrowserRouter>
- App.js
import { Routes, Route } from "react-router-dom";
import "./styles.css";
import Home from "./Home";
import Header from "./Header";
import PageNotFound from "./PageNotFound";
import About from "./About";
import Download from "./Download";
export default function App() {
let isViewTransition = "Opss, Your browser doesn't support View Transitions API";
if (document.startViewTransition) {
isViewTransition = "Yess, Your browser support View Transitions API";
}
return (
<>
<Header />
<Routes>
{/* Routes */}
<Route index element={<Home />} />
<Route path="About" element={<About />} />
<Route path="download" element={<Download />} />
{/* 404 page */}
<Route path="*" element={<PageNotFound />} />
</Routes>
<footer>
<a href="/">Complete tutorial on Medium</a>
<p>{isViewTransition}</p>
</footer>
</>
);
}
After routing all page to our <App> component, we will point each page to to corresponding component, All contained in <Routes>…</Routes>
{/* Routes */}
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="download" element={<Download />} />
{/* 404 page */}
<Route path="*" element={<PageNotFound />} />
Route “index” point to “/”, will show the <Home> component.
path about and download point to <About> and <Download> components respectively.
All the other paths “*” will point to <PageNotFound>c component.
Now, we did a check to show if our browser supports or not Our View Transitions API, and show the result at the bottom of every page, in our footer, By checking the “document.startViewTransition”.
let isViewTransition = "Oops, Your browser doesn't support View Transitions API";
if (document.startViewTransition) {
isViewTransition = "Yes, Your browser supports View Transitions API";
}
...
<footer>
<p>{isViewTransition}</p>
</footer>
...
return (
<>
<Header />
...
It’s here were we will implement our View Transitions API
- Header.js
import { useNavigate } from "react-router-dom";
import "./styles.css";
export default function Header() {
const navigate = useNavigate();
const viewNavigate = (newRoute) => {
// Navigate to the new route
if (!document.startViewTransition) {
return navigate(newRoute);
} else {
return document.startViewTransition(() => {
navigate(newRoute);
});
}
};
return (
<div className="header__container">
<span>ReactJs - React Router - View Transitions API</span>
<div className="link__container">
<button
onClick={() => {
viewNavigate("/");
}}
>
Home
</button>
<button
onClick={() => {
viewNavigate("/download");
}}
>
Download
</button>
<button
onClick={() => {
viewNavigate("/about");
}}
>
About
</button>
</div>
</div>
);
}
The main function in this tutorial is viewNavigate()
const navigate = useNavigate();
const viewNavigate = (newRoute) => {
// Navigate to the new route
if (!document.startViewTransition) {
return navigate(newRoute);
} else {
return document.startViewTransition(() => {
navigate(newRoute);
});
}
};
This function will encapsulate the ReactJS build-in navigate() function imported from useNavigate hook
If the browser do not support our View Transitions API, it will return a simple navigate() function with the parameter: newRoute
But when our browser supports our View transitions api, we will encapsulate our navigate(newRoute) with: document.startViewTransition() with one parameter; an arrow function.
return document.startViewTransition(() => {
navigate(newRoute);
});
When document.startViewTransitionis called:
→ The API captures the current state of the page including a screenshot. This includes taking a screenshot, which is async as it happens in the render steps of the event loop.
→ the API constructs a pseudo-element tree:
view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root)
└─ ::view-transition-new(root)
→ Then the call-back function is called: ”() => {navigate(newRoute)}”.
→ Capture the new state of our page.
→ Reconstruct the DOM to include the new elements.
→ All that happens asynchronously, the Rendering is paused, so the user doesn’t see a flash of the new content.
View Transitions flow
→ view-transition: sits on top of the animation as an overlay.
→ view-transition-group: Responsible for animating the size and position between the old and the new stats.
→ view-transition-image-pair: hold the current state of the animation.
→ The animation transits from the starting view (view-transition-image-old) to the new view (view-transition-image-new).
→ By default The animation is a cross-fade CSS transition.
→ The animation is done with CSS animations, both stats render as CSS ( the new CSS style will replace the old one), From opacity:1 to opacity:0 for the old view, then from opacity:0 to opacity:1 for the new view.
→ The root key means that the animation i’ll be applied to the entire page.
We can easily customise these transitions with CSS, we will talk about that in the next part of the tutorial, and will explain the style.css.
- Style.css
→ We will start by changing the default animation timing, the outgoing View go out fast and the incoming View come in slow
::view-transition-old(root) {
animation-duration: 0.5s;
}
::view-transition-new(root) {
animation-duration: 10s;
}
→ Then change the default animation, by creating two CSS keyframes; one for the old stat exit animation and the second for the new stat entrance.
@keyframes fade-and-scale-in {
from {
opacity: 0;
transform: scale(0);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes fade-and-scale-out {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0);
}
}
→ Now grouping all these properties using CSS Shorthand to have less code
/* Views Animation */
::view-transition-old(root) {
animation: fade-and-scale-out 0.5s ease-in-out 1 forwards;
}
::view-transition-new(root) {
animation: fade-and-scale-in 10s ease-in-out 1 forwards;
}
@keyframes fade-and-scale-in {
from {
opacity: 0;
transform: scale(0);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes fade-and-scale-out {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0);
}
}
/* Explanation of this line of CSS code */
{
animation: fade-and-scale-out 1s ease-in-out 1 forwards;
}
.fade-and-scale-out: This is the name of the animation. It's also used as the class name for the element that will be animated.
1s: This is the duration of the animation, which is set to 1 second.
ease-in-out: This is the timing function of the animation. In this case, it starts slowly, accelerates through the middle, and slows down at the end.
1: This is the number of times the animation will repeat. In this case, it will only run once.
forwards: This is the value of the animation-fill-mode property, which determines what styles are applied to the element before and after the animation runs. In this case, forwards means that the element will retain the styles from the last keyframe of the animation after the animation ends.
And this line too
{
animation: fade-and-scale-in 1s ease-in-out 1 forwards;
}
.fade-and-scale-in: This is the name of the animation. It's also used as the class name for the element that will be animated.
1s: This is the duration of the animation, which is set to 1 second.
ease-in-out: This is the timing function of the animation. In this case, it starts slowly, accelerates through the middle, and slows down at the end.
1: This is the number of times the animation will repeat. In this case, it will only run once.
forwards: This is the value of the animation-fill-mode property, which determines what styles are applied to the element before and after the animation runs. In this case, forwards means that the element will retain the styles from the last keyframe of the animation after the animation ends.
- Our Views or pages
We have three pages: Home, About and Download, all linked to their respective components.
The code is identical for these pages, will will take Home as example
import home from "./img/home.png";
export default function Home() {
return (
<main>
<h1>Home</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum
</p>
<img className="img" src={home} alt="Home" />
</main>
);
}
We have a <main> section with a <h1> title and paragraph <p> with a “lorem ipsum…” random text, and an image <img> imported as variable: home at the beginning of the file.
.header__container {
display: flex;
justify-content: space-between;
}
.link__container a {
margin-right: 10px;
}
.main__container {
width: 90%;
padding: 3px;
max-width: 600px;
margin: auto;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.img {
width: 300px;
height: 400px;
margin: auto;
width: 100%;
}
/* Views Animation */
::view-transition-old(root) {
animation: fade-and-scale-out 0.5s ease-in-out 1 forwards;
}
::view-transition-new(root) {
animation: fade-and-scale-in 10s ease-in-out 1 forwards;
}
@keyframes fade-and-scale-in {
from {
opacity: 0;
transform: scale(0);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes fade-and-scale-out {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0);
}
}
/* Second animation */
@keyframes slide-in {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slide-out {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-100%);
}
}
We have added a second CSS animation that you can try as a practice, go on try it and tell us what animation is best.