Skip to content
react-wrapped

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

Epic

Columns sharing one light, the ones that matter lit.

Rings

Epic
  • TypeScript55%
  • Go25%
  • CSS13%
  • Shell7%

Parts of a whole, biggest outside.

Bars

Epic

Columns sharing one light, the ones that matter lit.

Curtain, the cover this guide defines.

Installation

bash
pnpm add @react-wrapped/core gsap

Two 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.

transitions.tsx
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

Epic

Columns sharing one light, the ones that matter lit.

Rings

Epic
  • TypeScript55%
  • Go25%
  • CSS13%
  • Shell7%

Parts of a whole, biggest outside.

Bars

Epic

Columns 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:

tsx
// 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

KeyTypeWhat it does
idstringWhat a config names it by. Lower case letters, digits and hyphens, starting with a letter, and not a built-in id.
labelstringWhat 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) => voidPuts the transition on the timeline it is handed.
lucky?booleanLets the seed pick it for scenes nobody chose a transition for. Off unless you turn it on.

What play is handed

KeyTypeWhat it is
rootHTMLElementThe scene that is leaving. Your transition has to take it out of sight.
groupHTMLElement | nullThe 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.

KeyTypeWhat it is
hiddengsap.TweenVarsWhere a layer waits out of sight.
coveringgsap.TweenVarsThe frame is covered.
reopen?gsap.TweenVarsWhat the clearing starts from, when that is not where the covering ended.
clearedgsap.TweenVarsThe frame is clear again.
duration?numberSeconds 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-cut set to your id. Shape its layers from your own stylesheet with that selector, for example a clip-path on [data-cut="curtain"] i.
  • cover can 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. coverCut does 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 curtain does.

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.

Last updated . Read this page as Markdown.