create Scrim
Takes the nodes and how they animate, and returns a store that covers and uncovers them.
Usage
I write one store per scrim, in a config file. It lives outside React, so a route loader, the router or a plain function can call it.
// config/scrim.config.ts
export const SCRIM_NODE_CURTAIN = "curtain";
export const scrimRouteConfig = createScrim({
nodes: [SCRIM_NODE_CURTAIN],
animation: { duration: 650, easing: "cubic-bezier(0.65, 0, 0.35, 1)" },
enter: ({ curtain }, { play }) => play(curtain, [{ transform: "translateY(0)" }]),
leave: ({ curtain }, { play }) => play(curtain, [{ transform: "translateY(-100%)" }]),
});Nodes
Every element the store animates is a node. I name them in nodes, and each component registers its own with node(). The names are typed, so a typo doesn't compile.
// config/scrim.config.ts
export const SCRIM_NODE_CURTAIN = "curtain";
export const SCRIM_NODE_PAGE = "page";
export const scrimRouteConfig = createScrim({
nodes: [SCRIM_NODE_CURTAIN, SCRIM_NODE_PAGE],
// ...
});
// features/scrim/scrim_route.tsx
<div ref={scrimRouteConfig.node(SCRIM_NODE_CURTAIN)} className="fixed inset-0 z-9999 bg-main" />
// shared/components/sections/main.tsx
<main ref={scrimRouteConfig.node(SCRIM_NODE_PAGE)}>{children}</main>enter and status
status says where the scrim starts from. From idle it is off screen, so I give it a first keyframe. From leaving it was interrupted halfway, so I give only the target and it reverses from where it is.
enter: ({ curtain }, { status, play }) =>
play(
curtain,
status === "idle"
? [{ transform: "translateY(100%)" }, { transform: "translateY(0)" }]
: [{ transform: "translateY(0)" }],
),play
play runs a Web Animation with the defaults from animation, and any call can override them. Here the page rises in while the curtain leaves.
animation: { duration: 650, easing: "cubic-bezier(0.65, 0, 0.35, 1)" },
leave: ({ curtain, page }, { play }) =>
Promise.all([
play(curtain, [{ transform: "translateY(-100%)" }]),
play(
page,
[
{ opacity: 0, transform: "translateY(4rem)" },
{ opacity: 1, transform: "translateY(0)" },
],
{ delay: 500, duration: 900, easing: "cubic-bezier(0.22, 1, 0.36, 1)" },
),
]),until and hold
The store starts covered, so the server already sends the splash. The first leave waits for until, then for hold. Every later leave waits for hold only.
hold: 650,
until: () => document.fonts.ready,initial
A scrim that should wait for a call starts idle. I use it for the overlay of a global operation.
export const scrimSpinnerConfig = createScrim({
nodes: [SCRIM_NODE_OVERLAY],
initial: "idle",
animation: { duration: 350 },
enter: ({ overlay }, { play }) => play(overlay, [{ opacity: 1 }]),
leave: ({ overlay }, { play }) => play(overlay, [{ opacity: 0 }]),
});Other engines
The store awaits whatever enter and leave return. A GSAP tween or a Motion animation works in place of play.
enter: ({ curtain }) => gsap.to(curtain, { yPercent: 0, duration: 0.65, overwrite: true }),
leave: ({ curtain }) => gsap.to(curtain, { yPercent: -100, duration: 0.65, overwrite: true }),Reference
Options
| prop | type | default | |
|---|---|---|---|
nodes | readonly TName[] | — | the names of the nodes it animates |
enter | (nodes, { status, play }) => unknown | — | the cover animation |
leave | (nodes, { play }) => unknown | — | the uncover animation |
initial? | "covered" | "idle" | "covered" | covered for a splash, idle for a scrim that waits for a call |
animation? | KeyframeAnimationOptions | — | the defaults of every play call |
hold? | number | 0 | milliseconds to stay covered before leaving |
until? | (nodes, { play }) => unknown | — | what the first leave waits for |
Returns
| prop | type | default | |
|---|---|---|---|
node | (name: TName) => ref | — | the ref that registers a node |
cover | () => Promise<void> | — | runs enter; resolves once covered |
uncover | () => Promise<void> | — | waits for until and hold, then runs leave |
subscribe | (listener) => () => void | — | listens to every change |
getStatus | () => ScrimStatus | — | idle, entering, covered or leaving |
getIsReady | () => boolean | — | true once until has resolved |