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.
:bulb: 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.
usePageTransition()
with transition status that can be accessed by any component in the tree
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.
<PageTransitions>
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.
:warning:
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 |
usePageTransition()
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 ...
}