A framework-agnostic page transition lib for react. Tested with CRA, Next.js, Gatsby.js
As a creative studio we often have to adapt to our clients. It would be great if we could keep the same mental model and lifecycle for page transitions, no matter the react framework we end up using.
💡 This library doesn’t apply any styles to the page container, you need to manage it by yourself with an animation library or CSS transitions/animations.
Depends on react-transition-group
for mounting/unmounting logic.
Check out the demo and see the code.
- Should work with most common React frameworks
- Doesn't lock you in to any specific animation library
- Adds CSS classnames with transition status to the page containers. CSS animations are more performant than JS, especially while the browser is loading assets.
- Provides a hook
usePageTransition()
with transition status that can be accessed by any component in the tree - Relies on React.Suspense for delaying transitions if a page takes time to load
- Supports default page transitions on history nav
- Supports contextual transitions triggered by links - able to pass data to current and next page
- Support barba.js sync mode - i.e. having 2 pages mounted at the same time (overlapping)
- Doesn't touch scroll position - you need to manage it yourself depending on your framework and transition timings.
yarn add @14islands/react-page-transitions
// App.js
return (
<PageTransitions pageName={location.pathname}>
<Routes location={location}>
<Route path="/" element={<PageHome />} />
<Route path="/a" element={<PageA />} />
<Route path="/b" element={<PageB />} />
</Routes>
</PageTransitions>
.page-appear,
.page-enter {
opacity: 0;
}
.page-enter-active,
.page-appear-active {
opacity: 1;
transition: opacity 1s;
}
.page-exit {
opacity: 1;
}
.page-exit-active {
opacity: 0;
transition: opacity 1s;
}
function MyComponent() {
const { transitionState, from, to } = usePageTransition();
const styles = useSpring({
from: {
x: transitionState === "entering" ? "-100%" : "0%",
},
});
return <animated.div style={styles} />;
}
The following sequence describes a page loading for the first time.
sequenceDiagram
participant suspended
participant appear
participant appearing
participant appeared
Note over suspended, appeared: Page A
Note right of suspended: .page-appear-suspended
Note right of appear: .page-appear
appear->>appearing: onEnter({ isAppearing: true })
Note right of appearing: .page-appear-active
appearing->>appeared: onEntering({ isAppearing: true, done })
Note right of appeared: .page-appear-done
appeared->>appeared: onEntered({ isAppearing: true })
The suspended
state only happens if the page suspended while mounting. It continues to the appear
state automatically when suspense has resolved. To prevent this you can define your own <Suspense>
further down the tree, which will cause the page to animate in and show your fallback instead.
The following sequence describes a page transition from "Page A" to "PageB" using the default mode "out-in". The current page exits before the new page enters.
sequenceDiagram
participant exit
participant exiting
participant suspended
Note over exit,suspended: Page A
participant enter
participant entering
participant entered
Note right of exit: .page-exit
exit->>exiting: onExit()
Note right of exiting: .page-exit-active
exiting->>suspended: onExiting({ done })
Note over suspended,entered: Page B
Note right of suspended: .page-enter-suspended
Note right of enter: .page-enter
enter->>entering: onEnter()
Note right of entering: .page-enter-active
entering->>entered: onEntering({ done })
Note right of entered: .page-enter-done
entered->>entered: onEntered()
The suspended
state only happens if the entering page (Page B) suspended while mounting. It continues to the appear
state automatically when suspense has resolved. To prevent this you can define your own <Suspense>
further down the tree, which will cause the page to animate in and show your fallback instead.
Wrap your routes with this component so it can manage the lifecycle of mounting and unmounting pages.
By default it uses the "out-in" strategy, which means the current page animates out and is removed from the DOM before the new page is mounted and animates in.
It listens to CSS transitionend
and CSS animationend
events on the page container by default to trigger the lifecycle. This can be turned off either globally using the properties, or overridden per page via the onExiting
and onEntering
callbacks on the usePageTransitions
hook.
⚠️ transitionend
andanimationend
will bubble up from the page children. Whichever animation or transition finish first, will mark the page transition as done.
If you want to take manual control of the transition duration, you can use the callbacks onEntering
and onExiting
on the usePageTransition
hook.
Property | type | default |
---|---|---|
mode | out-in | in-out | sync | "out-in" |
className | string | "page" |
timeout | number | undefined |
detectAnimationEnd | boolean | true |
detectTransitionEnd | boolean | true |
Show React Router v6 CRA example
// App.js
return (
<PageTransitions pageName={location.pathname}>
// Note: need to pass location to Routes
<Routes location={location}>
<Route path="/" element={<PageHome />} />
<Route path="/a" element={<PageA />} />
<Route path="/b" element={<PageB />} />
</Routes>
</PageTransitions>
Show Next.js example
// _app.js
function MyApp({ Component, pageProps, router }) {
console.log("router.pathname", router.pathname);
return (
<PageTransitions pageName={router.pathname}>
<Component {...pageProps} />
</PageTransitions>
);
}
Show Gatsby.js example
// gatsby-browser.js
export const wrapPageElement = ({ element, props: { location } }) => {
return (
<PageTransitions pageName={location.pathname}>{element}</PageTransitions>
);
};
This hook allows any component nested within <PageTransitions>
to listen and react to the transition state. It can also accept callbacks that will fire during the transition lifecycle.
function MyPage() {
const { transitionState, from, to, data } = usePageTransition({
onEnter: ({ from, to }) => void,
onEntering: ({ from, to, done, data }) => void
onEntered: ({ from, to }) => void,
onExit: ({ from, to }) => void,
onExiting: ({ from, to, done, data }) => void
})
}
transitionState | description |
---|---|
suspended | Page is waiting to mount |
appear | Page is about to animate in on page load |
appearing | Page is currently animating in on page load |
appeared | Page has finished animating in on page load |
exit | When page is about to animate out |
exiting | Page is currently animating out |
exited | Page has finished animating out |
enter | Page was mounted and is about to enter |
entering | Page is currently animating in |
entered | Page has finished animating in |
callbacks | callback params | description |
---|---|---|
onEnter | { isAppearing, from, to, data } | Page was mounted and is about to enter |
onEntering | { isAppearing, from, to, done, data } | Page is currently animating in - the done function has to be called to indicated animation is complete |
onEntered | { isAppearing, from, to, data } | Page finished animating in after page navigation |
onExit | { from, to } | Page is about to exit due to page navigation |
onExiting | { from, to, done } | Page is currently animating out - the done function has to be called to indicated animation is complete |
This function can be used to pass state to the pages exiting and entering. This enables contextual transitions for when clicking on specific links.
setPageTransitionData(data: any): void
The user is in control of figuring out what to do with that data.
function HomePage() {
const { transitionState, from, to } = usePageTransition({
onExiting: async ({ from, to, done, data }) => {
// `data` is what was passed to setPageTransitionData()
if (data === "home-list-item") {
await animationOutContextual()
} else {
await animationOutDefault()
}
done()
}
})
return (
<div>
{ items.map( item =>
<Link to={item.url} onClick={() => setPageTransitionData("home-list-item")}>
{item.label}
</Link>
)}
</div>
)
}
function ItemPage() {
const { transitionState, from, to } = usePageTransition({
onEntering: async ({ from, to, done, data }) => {
// `data` is what was passed to setPageTransitionData()
if (data === "home-list-item") {
await animationInContextual()
} else {
await animationInDefault()
}
done()
}
})
return ...
}
How do I detect if user navigated using back button?
This depends on your framework router. ReactRouter v6, for instance, provides a `useNavigationType()` hook that tells you if it was a POP, PUSH or REPLACE
How do I scroll to top when navigating to a new page?
This is up to the user to control. It depends on the router you are using and the timing of your transition. Here's an example using ReactRouter v6, which let's the browser reset previous scroll position on history navigation:
import { useLocation, useNavigationType } from "react-router-dom";
function ScrollToTop() {
const location = useLocation();
const action = useNavigationType();
useEffect(
function scrollToTopWhenNavForward() {
if (action !== "POP") {
window.scrollTo(0, 0);
}
},
[location, action]
);
return null;
}