First I recommend reading Greensock’s React documentation.
Intro Animations in Next.JS
For intro animations, if you use useLayoutEffect with SSR your console will fill up with warnings. To avoid this apply useIsomorphicLayoutEffect instead. Go to useIsomorphicLayoutEffect.
To prevent the flash of unstyled content (FOUC) with SSR, you need to set the initial styling state of the component. For example, if we are fading in, the initial style of that component should be an opacity of zero.
Outro Animations in Next.JS
For outro animations, intercept the page transition, and do the exit animations, then onComplete route to the next page.
To pull this off, we can use TransitionLayout higher order component as a wrapper to delay the routing change until after any animations have completed, and a TransitionProvider component that will take advantage of React’s useContext hook to share an outro timeline across multiple components, regardless of where they are nested.
Transition Context
In order to make a page transition effect, we need to prevent rendering the new page before our outro animation is done.
We may have many components with different animation effects nested in our pages. To keep track of all the different outro transitions, we will use a combination of React’s Context API and a top-level GSAP timeline.
In TransitionContext we will create our TransitionProvider which will make our GSAP timeline for outro animations available to any components who would like to transition out during a page change.
import React, { useState, createContext, useCallback } from "react"
import gsap from "gsap"
const TransitionContext = createContext({})
const TransitionProvider = ({ children }) => {
const [timeline, setTimeline] = useState(() =>
gsap.timeline({ paused: true })
)
return (
<TransitionContext.Provider
value={{
timeline,
setTimeline,
}}
>
{children}
</TransitionContext.Provider>
)
}
export { TransitionContext, TransitionProvider }
Next, we have TransitionLayout which will be our controller that will initiate the outro animations and update the page when they are all complete.
import { gsap } from "gsap"
import { TransitionContext } from "../context/TransitionContext"
import { useState, useContext, useRef } from "react"
import useIsomorphicLayoutEffect from "../animation/useIsomorphicLayoutEffect"
export default function TransitionLayout({ children }) {
const [displayChildren, setDisplayChildren] = useState(children)
const { timeline, background } = useContext(TransitionContext)
const el = useRef()
useIsomorphicLayoutEffect(() => {
if (children !== displayChildren) {
if (timeline.duration() === 0) {
// there are no outro animations, so immediately transition
setDisplayChildren(children)
} else {
timeline.play().then(() => {
// outro complete so reset to an empty paused timeline
timeline.seek(0).pause().clear()
setDisplayChildren(children)
})
}
}
}, [children])
return <div ref={el}>{displayChildren}</div>
}
In a custom App component, we can have TransitionProvider and TransitionLayout wrap the other elements so they can access the TransitionContext properties. Header and Footer exist outside of Component so that they will be static after the initial page load.
import { TransitionProvider } from "../src/context/TransitionContext"
import TransitionLayout from "../src/animation/TransitionLayout"
import { Box } from "theme-ui"
import Header from "../src/ui/Header"
import Footer from "../src/ui/Footer"
export default function MyApp({ Component, pageProps }) {
return (
<TransitionProvider>
<TransitionLayout>
<Box
sx={{
display: "flex",
minHeight: "100vh",
flexDirection: "column",
}}
>
<Header />
<Component {...pageProps} />
<Footer />
</Box>
</TransitionLayout>
</TransitionProvider>
)
}
Component-Level Animation
Here is an example of a basic animation we can do at the component level. We can add as many of these as we want to a page and they will all do the same thing, wrap all its children in a transparent div and fade it in on page load, then fade out when navigating to a different page.
import { useRef, useContext } from "react"
import { gsap } from "gsap"
import { Box } from "theme-ui"
import useIsomorphicLayoutEffect from "./useIsomorphicLayoutEffect"
import { TransitionContext } from "../context/TransitionContext"
const FadeInOut = ({ children }) => (
const { timeline } = useContext(TransitionContext)
const el = useRef()
// useIsomorphicLayoutEffect to avoid console warnings
useIsomorphicLayoutEffect(() => {
// intro animation will play immediately
gsap.to(el.current, {
opacity: 1,
duration: 1,
})
// add outro animation to top-level outro animation timeline
timeline.add(
gsap.to(el.current, {
opacity: 1,
duration: .5,
}),
0
)
}, [])
// set initial opacity to 0 to avoid FOUC for SSR
<Box ref={el} sx={{opacity: 0}}>
{children}
</Box>
)
export default FadeInOut
We can take this pattern and extract it into an extendable AnimateInOut helper component for reusable intro/outro animation patterns in our app.
import React, { useRef, useContext } from "react"
import { gsap } from "gsap"
import { Box } from "theme-ui"
import useIsomorphicLayoutEffect from "./useIsomorphicLayoutEffect"
import { TransitionContext } from "../context/TransitionContext"
const AnimateInOut = ({
children,
as,
from,
to,
durationIn,
durationOut,
delay,
delayOut,
set,
skipOutro,
}) => {
const { timeline } = useContext(TransitionContext)
const el = useRef()
useIsomorphicLayoutEffect(() => {
// intro animation
if (set) {
gsap.set(el.current, { ...set })
}
gsap.to(el.current, {
...to,
delay: delay || 0,
duration: durationIn,
})
// outro animation
if (!skipOutro) {
timeline.add(
gsap.to(el.current, {
...from,
delay: delayOut || 0,
duration: durationOut,
}),
0
)
}
}, [])
return (
<Box as={as} sx={from} ref={el}>
{children}
</Box>
)
}
export default AnimateInOut
The AnimateInOut component has built in flexibility for different scenarios:
- Setting different animations, durations and delays for intros and outros
- Skipping the outro
- Setting the element tag for the wrapper, e.g. use a
<span> instead of a <div>
- Use GSAP’s
set option to define initial values for the intro
Using this we can create all sorts of reusable intro/outro animations, such as <FlyInOut>, <ScaleInOut>, <RotateInOut3D> and so forth.
I have a demo project where you can see the above in practice: TweenPages