mekuri
mekuri copied to clipboard
📕 Page transition animation for React
mekuri
mekuri
is React package for page transition animations with wait
and sync
modes, supporting popstate
and scroll restoration
.
Customize animations per component with the useMekuri
hook and use with libraries like GSAP
. It integrates with Next.js and Remix; can also be integrated with Next.js App Router
, but for stable operation it is recommended to use Pages Router rather than App Router. Next.js Pages Router demo
$ npm i @funtech-inc/mekuri
Features 📕
-
wait
andsync
modes -
scrollRestoration
in popstate. - When in
sync
mode, routing is possible inwait
mode when in popstate. - Supports frameworks such as
Next.js
andRemix
. Can also integrate withNext.js App Router
. -
useMekuri
hook for each component. - Integration into smooth scrolling libraries such as lenis is also possible.
Usage
export default function App({ Component, pageProps }: AppProps) {
const { pathname } = useRouter();
return (
<MekuriContext trigger={pathname}>
<SomeAnimationComponent>
<Mekuri>
<Component
key={`${pathname + performance.now()}`}
{...pageProps}
/>
</Mekuri>
</SomeAnimationComponent>
</MekuriContext>
);
}
MekuriContext
The context to wrap the whole thing in. Set the trigger
to a state
to switch content. You can use pathname
if you want to use it as a page transition.
Mekuri
Unmounting of children can be delayed by wrapping them in a Mekuri
component.
useMekuri
Hooks that can be called within MekuriContext
. Callbacks include onOnce
, onLeave
, onEnter
, onAfterSyncEnter
, onEveryLeave
and onEveryEnter
.
const SomeAnimationComponent = ({
children,
}: {
children: React.ReactNode;
}) => {
const ref = useRef<HTMLDivElement>(null);
useMekuri({
onLeave: (props: MekuriCallbackProps) => {
gsap.to(ref.current, {
opacity: 0,
});
},
onEnter: (props: MekuriCallbackProps) => {
gsap.to(ref.current, {
opacity: 1,
});
},
});
return <div ref={ref}>{children}</div>;
};
Each callback has MekuriCallbackProps
as an argument.
type MekuriCallbackProps = {
prevTrigger: Trigger | null | undefined;
currentTrigger: Trigger | null | undefined;
nextTrigger: Trigger | null | undefined;
/** Returns the Y position before leaving the page */
yPosBeforeLeave: number;
/** If # is attached to the URL when transitioning, the distance to that ID is returned. */
getHashPos: ReturnHashPosReturn;
/** intersectionObserver (
targetRef: React.RefObject<HTMLElement>,
callback: (isIntersecting: boolean) => void
) => void
* */
intersectionObserver: HandleIntersectionObserver;
/** mekuri renders based on timeout. Therefore, there are cases where the next component is rendered before the chunked Stylesheet updated by Next.js is loaded. `onStylesheetLoad` ensures that functions are executed after the Stylesheet is loaded. `onStylesheetLoad` ensures that the function is executed after the Stylesheet is loaded */
onStylesheetLoad: (callback: () => void) => void;
/** Whether the transition is by popstate */
isPopstate: boolean;
};
useMekuriDuration
It is possible to receive the duration
set in the MekuriContext
.
const { millisecond, second } = useMekuriDuration();
useMekuriTrigger
phase
: enter
| leave
, Specify the phase to subscribe to trigger updates
const trigger = useMekuriTrigger(phase);
integrate with Next.js App Router
Since the key cannot be obtained by changing children during app router page transition, it is necessary to import LayoutRouterContext
from next and pass the context to MekuriFreezer
.
For more information on App Router
page transition animations, see the following issue.
See this issue
MekuriFreezer
"use client";
import { MekuriFreezer, Mekuri } from "@/packages/mekuri/src";
// import { LayoutRouterContext } from "next/dist/shared/lib/app-router-context";
// Next.js ^13.5.2
import { LayoutRouterContext } from "next/dist/shared/lib/app-router-context.shared-runtime";
import { usePathname } from "next/navigation";
export const PageTransitionLayout = ({
children,
}: {
children: React.ReactNode;
}) => {
const pathname = usePathname();
return (
<Mekuri>
<MekuriFreezer
key={`${pathname + performance.now()}`}
routerContext={LayoutRouterContext}>
{children}
</MekuriFreezer>
</Mekuri>
);
};
integrate with Remix
export default function App() {
const location = useLocation();
const outlet = useOutlet();
return (
<html lang="en" className="h-full">
<head></head>
<body className="h-full">
<MekuriContext trigger={location.pathname}>
<SomeAnimationComponent>
<Mekuri>
<div key={location.pathname}>{outlet}</div>
</Mekuri>
</SomeAnimationComponent>
</MekuriContext>
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}