Custom transition
A transition takes one scene out and says when the next one may start. defineTransition adds yours to the nineteen built-in ones: it can be picked per scene in the config, it shows up in the scene editor, and the seed can deal it out if you let it. This guide adds two, a cover and a move.
Bars
EpicColumns sharing one light, the ones that matter lit.
Rings
Epic- TypeScript55%
- Go25%
- CSS13%
- Shell7%
Parts of a whole, biggest outside.
Bars
EpicColumns sharing one light, the ones that matter lit.
Curtain, the cover this guide defines.
Installation
pnpm add @react-wrapped/core gsapTwo transitions
This file is registered on this site. curtain is a cover: it draws three layers over the frame. tilt is a move: it draws nothing and animates the leaving scene itself.
import { coverCut, defineTransition, HANDOVER } from "@react-wrapped/core";
/**
* A cover: three layers fall from the top like a curtain, the old scene leaves behind them, and they fall on out of
* the bottom of the frame. The stage colours the three `<i />` with the palette's levels two, three and four.
*/
export const curtain = defineTransition({
id: "curtain",
label: "Curtain",
cover: (
<>
<i />
<i />
<i />
</>
),
play: (tl, { root, group }) => {
// A stage that mounted before this transition was defined has no cover for it. Fade, so the scene still leaves.
if (!group) {
tl.to(root, { autoAlpha: 0, duration: 0.4 });
return;
}
coverCut(
tl,
{ root, group },
// Where a layer waits, where it covers the frame, and where it ends up once the frame is clear again.
{ hidden: { yPercent: -101 }, covering: { yPercent: 0 }, cleared: { yPercent: 101 }, duration: 0.55 },
);
},
});
/** A move: nothing is drawn over the frame. The old scene itself tips over and slides out to the left. */
export const tilt = defineTransition({
id: "tilt",
label: "Tilt",
play: (tl, { root }) => {
tl.to(root, { autoAlpha: 0, xPercent: -30, rotate: -6, duration: 0.5, ease: "power3.in" });
// The next scene starts here, as the last of this one leaves, so two headlines never share the frame.
tl.addLabel(HANDOVER, "-=0.07");
},
});Bars
EpicColumns sharing one light, the ones that matter lit.
Rings
Epic- TypeScript55%
- Go25%
- CSS13%
- Shell7%
Parts of a whole, biggest outside.
Bars
EpicColumns sharing one light, the ones that matter lit.
Tilt, the move.
Register it once
defineTransition has to run before a stage is on the page, because a stage draws every transition's cover when it mounts. Import the file where your app starts. From then on the id works like a built-in one:
// main.tsx, or your root layout: before anything renders a stage.
import "./transitions";
import { DEFAULT_WRAPPED_CONFIG, type WrappedConfig } from "@react-wrapped/core";
// The scene with the key "total" leaves with the curtain.
export const config: WrappedConfig = { ...DEFAULT_WRAPPED_CONFIG, cuts: { total: "curtain" } };transitionList()returns every transition as{ id, label }, the built-in ones first and then yours. The scene editor offers exactly that list.isTransition(id)says whether an id is known. A link that names an unknown transition falls back to the seed's pick, so define yours before you read a config from the URL.- Defining an id twice replaces the first one. Using a built-in id, or one with capitals or spaces, throws.
defineTransition
| Key | Type | What it does |
|---|---|---|
id | string | What a config names it by. Lower case letters, digits and hyphens, starting with a letter, and not a built-in id. |
label | string | What the scene editor calls it. |
cover? | ReactNode | ((format: WrappedFormat) => ReactNode) | Markup the transition draws above the scenes. Rendered once into every stage, hidden until you show it. |
play | (tl: gsap.core.Timeline, context: TransitionContext) => void | Puts the transition on the timeline it is handed. |
lucky? | boolean | Lets the seed pick it for scenes nobody chose a transition for. Off unless you turn it on. |
What play is handed
| Key | Type | What it is |
|---|---|---|
root | HTMLElement | The scene that is leaving. Your transition has to take it out of sight. |
group | HTMLElement | null | The element holding your cover, or null when there is none in this stage. |
A cover with coverCut
Every built-in cover is one function with different numbers, and it is exported. coverCut(tl, { root, group }, move) brings the layers of group in one after another until the frame is covered, hides the old scene under them, marks the handover, and clears the layers in reverse order onto the next scene. You only describe three states of a layer.
| Key | Type | What it is |
|---|---|---|
hidden | gsap.TweenVars | Where a layer waits out of sight. |
covering | gsap.TweenVars | The frame is covered. |
reopen? | gsap.TweenVars | What the clearing starts from, when that is not where the covering ended. |
cleared | gsap.TweenVars | The frame is clear again. |
duration? | number | Seconds for each layer's way in, and the same for its way out. Half a second when left out. |
ease? | readonly [string, string] | One ease for the way in and one for the way out. |
- The stage gives the first three children of your cover the palette's levels two, three and four as their colour. Three
<i />are enough: each already fills the frame. - Layers follow each other by 0.085 seconds, so a cover's edge is a band of three colours and never one hard line.
- Your cover sits in an element with
data-cutset to your id. Shape its layers from your own stylesheet with that selector, for example aclip-pathon[data-cut="curtain"] i. covercan be a function of the format, for shapes that should not stretch in the wide frame.
A move and the HANDOVER label
A move animates root, the scene that is leaving, and has to end with it out of sight. Add the label HANDOVER where the next scene should start. In tilt that is 0.07 seconds before the end, so the next headline comes in as the last of the old one goes. With no label the next scene starts a quarter of a second before your timeline ends. coverCut sets the label for you, just before the cover closes.
The rules
- Only add tweens to `tl`. The player seeks and reverses it, and the scrubber runs through your transition in both directions.
- Animate only `root` and your own `group`. The next scene and the chrome are not yours.
- Leave `root` hidden when you are done:
autoAlpha: 0.coverCutdoes it under the closed cover. - Handle a missing `group`. A stage that mounted before your transition was defined has no cover for it. Fade the scene out, as
curtaindoes.
Let the seed pick it
With lucky: true your transition joins the pool the seed draws from for scenes nobody picked a transition for. Decide this before you share links: the pool's size is part of the draw, so adding a lucky transition changes which one most scenes of an existing video get.
