Simple page transition in NextJS with GSAP and react-transition-group
06 August 2022
We're going to learn one way of animating page transitions in NextJS with two libraries: GSAP and react-transition-group.
Prerequisites
- Basic knowledge of NextJS and its page routing
- Basic knowledge of React
- Know how to install a module with NPM
Creating the project
Create our NextJS app with create-next-app. We will name it page-transition.
npx create-next-app page-transition
Now you can cd (enter) inside our newly created and we can proceed to install GSAP, our animation library, and react-transition-group to listen to page transitions.
cd page-transition npm i gsap react-transition-group
I will be using tailwind to speed up the process of styling this small project, but feel free to use CSS to style it as you please.
You can follow tailwind official guide to install it in your NextJS project in case you want to use it as well.
Start the server.
npm run dev
When you go to http://localhost:3000/ you should now see your NextJS starter project.
Creating a layout
We will now create a layout that we can reuse on our different pages. Create a new folder at the root of your directory called components, and inside it, we can create a new file called Header.jsx and another one calledLayout.jsx.
In Header.jsx, we create a simple header that will have a title on the left and links to our pages on the right:
// components/Header.jsx import Link from 'next/link' const Header = () => { return ( <header className='max-w-5xl flex justify-between mx-auto mb-8 px-4 py-8 border-b border-black/20'> <h1 className='text-2xl font-semibold'>Page Transition</h1> <nav className='flex gap-4 text-lg text-blue-700'> <Link href='/'>Home</Link> <Link href='/page1'>Page 1</Link> <Link href='/page2'>Page 2</Link> </nav> </header> ) } export default Header
We are using next/link for our internal routing. If you want to add another HTML element inside the Link component, be sure to add a property called passHref to the Link.
Now let's import the header inside our Layout component, and then actually create the layout that we can re-use :
// components/Layout.jsx import Header from './Header' const Layout = ({ children }) => { return ( <> <Header /> <main className='p-4 mx-auto max-w-4xl'>{children}</main> </> ) } export default Layout
We are passing children as a prop, which will be everything that is nested inside our page layout.
Using our layout
Now that we have our base layout created, we can wrap our whole app inside so that every page can share the header and our main style.
In _app.js, import our Layout and wrap it around our pageProps
// pages/_app.js import Layout from '../components/layout' import '../styles/globals.css' function MyApp({ Component, pageProps }) { return ( <Layout> <Component {...pageProps} /> </Layout> ) } export default MyApp
Creating our pages
We already have a default home page called index.js, but we want more! So we are going to create 2 additional pages in the same folder. Let's call them page1.js and page2.js.
Our styled home page looks like this.
// pages/index.js export default function Home() { return ( <section className='flex flex-col gap-8 items-center'> <h2 className='text-4xl font-semibold'>Home page</h2> <p> Lorem ipsum dolor sit amet consectetur adipisicing elit. Nobis dolor aspernatur natus at totam quae temporibus, optio mollitia numquam est ab ut non et omnis quas in, laudantium nemo. Nisi, culpa. Porro ab, ducimus rerum, sed eum facilis dolorum a veniam cumque sit molestias quaerat nobis! Nulla veritatis ea eius! </p> <p> Lorem ipsum dolor sit amet consectetur adipisicing elit. Ullam voluptatem voluptas ipsa voluptatibus nihil totam a sequi vel tempore incidunt. Possimus, obcaecati soluta non in nostrum eaque nemo quod adipisci eos architecto expedita eum quis amet dolorem voluptas quas impedit voluptatem aperiam eveniet, reiciendis, minima ex. Consectetur totam voluptas quisquam. </p> </section> ) }
page1.js and page2.js are the same, only the text inside our H2 changes.
This is page1.js
// pages/page1.js export default function Page1() { return ( <section className='flex flex-col gap-8 items-center'> <h2 className='text-4xl font-semibold bg-red-200 px-4 py-2'> First Page </h2> <p> Lorem ipsum dolor sit amet consectetur adipisicing elit. Nobis dolor aspernatur natus at totam quae temporibus, optio mollitia numquam est ab ut non et omnis quas in, laudantium nemo. Nisi, culpa. Porro ab, ducimus rerum, sed eum facilis dolorum a veniam cumque sit molestias quaerat nobis! Nulla veritatis ea eius! </p> <p> Lorem ipsum dolor sit amet consectetur adipisicing elit. Ullam voluptatem voluptas ipsa voluptatibus nihil totam a sequi vel tempore incidunt. Possimus, obcaecati soluta non in nostrum eaque nemo quod adipisci eos architecto expedita eum quis amet dolorem voluptas quas impedit voluptatem aperiam eveniet, reiciendis, minima ex. Consectetur totam voluptas quisquam. </p> </section> ) }
And this is page2.js
// pages/page2.js export default function Page2() { return ( <section className='flex flex-col gap-8 items-center'> <h2 className='text-4xl font-semibold bg-yellow-200 px-4 py-2'> Second Page </h2> <p> Lorem ipsum dolor sit amet consectetur adipisicing elit. Nobis dolor aspernatur natus at totam quae temporibus, optio mollitia numquam est ab ut non et omnis quas in, laudantium nemo. Nisi, culpa. Porro ab, ducimus rerum, sed eum facilis dolorum a veniam cumque sit molestias quaerat nobis! Nulla veritatis ea eius! </p> <p> Lorem ipsum dolor sit amet consectetur adipisicing elit. Ullam voluptatem voluptas ipsa voluptatibus nihil totam a sequi vel tempore incidunt. Possimus, obcaecati soluta non in nostrum eaque nemo quod adipisci eos architecto expedita eum quis amet dolorem voluptas quas impedit voluptatem aperiam eveniet, reiciendis, minima ex. Consectetur totam voluptas quisquam. </p> </section> ) }
Your app should now have a working routing system when you click on the links inside the header.
Creating the animations
Now comes the fun part, we will animate the transitions using GSAP and react-transition-group.
Head back to your Layout.jsx file and import our two libraries as well as nextjs/router useRouter.
import { SwitchTransition, Transition } from 'react-transition-group' import gsap from 'gsap' import { useRouter } from 'next/router'
We are now going to create two methods inside our Layout component. These functions will be the animations when our page enters and when it exits. We are using gsap.fromTo() method, which takes in our element as the first parameter, and then two objects with our initial state and our end state.
We are animating the y property (which interpolates translateY) and autoAlpha, which is similar to opacity but sets the visibility to hidden when the opacity reaches 0. The ease is set to power3 (which is a GSAP-specific easing)
const onPageEnter = (node) => { gsap.fromTo( node, { y: 50, autoAlpha: 0, ease: 'power3', }, { y: 0, autoAlpha: 1, duration: 1, ease: 'power3', } ) } const onPageExit = (node) => { gsap.fromTo( node, { y: 0, autoAlpha: 1, ease: 'power3.out', }, { y: -50, autoAlpha: 0, duration: 0.5, ease: 'power3.inOut', } ) }
Animate our route change
We will now use react-transition-group SwitchTransition and Transition components to manage our page transitions.
In our Layout component, we need to wrap our main element inside Transition, and Transitioninside SwitchTransition. We are not including the header inside it because we don't want to animate it every time our route changes.
<SwitchTransition> <Transition> <main className='p-4 mx-auto max-w-4xl'>{children}</main> </Transition> </SwitchTransition>
The Transition component will accept a few arguments. You can find them on the documentation to get more info on what they do.
We are going to use the following:
<Transition key={router.asPath} // our route as a key timeout={500} in={true} onEnter={onPageEnter} // our enter animation onExit={onPageExit} // our exit animation mountOnEnter={true} unmountOnExit={true}> <main className='p-4 mx-auto max-w-4xl'>{children}</main> </Transition>
Notice how our Transition component has a router object passed inside as the key. You can just declare it at the top of our component like so:
const router = useRouter()
This is what your Layout.jsx should look like now:
// components/Layout.jsx import { useRouter } from 'next/router' import { SwitchTransition, Transition } from 'react-transition-group' import gsap from 'gsap' import Header from './Header' const Layout = ({ children }) => { const router = useRouter() const onPageEnter = (element) => { gsap.fromTo( element, { y: 50, autoAlpha: 0, ease: 'power3.out', }, { y: 0, autoAlpha: 1, duration: 1, ease: 'power3.out', } ) } const onPageExit = (element) => { gsap.fromTo( element, { y: 0, autoAlpha: 1, ease: 'power3.out', }, { y: -50, autoAlpha: 0, duration: 0.5, ease: 'power3.inOut', } ) } return ( <> <Header /> <SwitchTransition> <Transition key={router.pathname} timeout={500} in={true} onEnter={onPageEnter} onExit={onPageExit} mountOnEnter={true} unmountOnExit={true}> <main className='p-4 mx-auto max-w-4xl'>{children}</main> </Transition> </SwitchTransition> </> ) } export default Layout
Voilà!
We are all done, now you can enjoy a nice simple transition between your different pages where the component smoothly fades up and then comes back from the bottom. Feel free to play around with GSAP tweens until you find a transition you like!
You can check out the GitHub repository from this tutorial here