Skip to content

About

馃巻聽Year in review videos in React. Animated scenes, 21 charts, 19 transitions, a player with a timeline editor, a year scan and share cards, all on one GSAP timeline. The engine behind Hype Stack's Wrapped.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

聽

History

33 Commits

Folders and files

NameName
Last commit message
Last commit date
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽
聽

Repository files navigation

Year in review videos, in React.

See it live on Hype Stack | Docs | Designs | Discord


Join our Discord community


What it is

react-wrapped turns a year of numbers into a short video that plays in the page. You describe the year as a list of scenes. The library brings the stage, the animation, the transitions between scenes, a player with a timeline you can edit, and a button that saves the video as a file. It started as the engine behind Hype Stack's Wrapped, where it plays real developer years. Use it whole as one component, or take single packages: only the charts, only the player, only the share cards.

A Wrapped playing in the react-wrapped player: 1,687 contributions over a year grid, with the timeline under the video

Install

pnpm add @react-wrapped/kit gsap

Load the stylesheet once, anywhere in your app:

import "@react-wrapped/kit/styles.css";

You need React 19 and GSAP 3.13 or newer. The stylesheet brings everything the components use and keeps to its own wr- classes, so there is no Tailwind to set up and no reset to load.

Quick start

A Wrapped is a subject, a seed and a list of scenes. Each scene is a function you call with its content and a key.

import { useState } from "react";
import { bigNumberScene, chartScene, DEFAULT_WRAPPED_CONFIG, summaryScene, WrappedView } from "@react-wrapped/kit";
import "@react-wrapped/kit/styles.css";

const subject = { handle: "ada", name: "Ada", avatarUrl: null, caption: "Orbit Wrapped", period: "2026" };
const weeks = [4, 9, 6, 14, 22, 31, 18, 12, 9, 15, 7, 11];

const scenes = [
  bigNumberScene(
    { eyebrow: "This year", value: 1687, title: ["commits."], lede: "**4.6** a day.", counter: "climb", variant: 0 },
    { key: "total", label: "Total" },
  ),
  chartScene(
    {
      eyebrow: "Your year",
      title: ["Then came", "*launch week.*"],
      lede: "The busiest stretch, lit.",
      chart: { type: "spark", values: weeks, band: { from: 4, to: 6, label: "Launch week" } },
    },
    { key: "launch", label: "Launch week" },
  ),
  summaryScene(
    { handle: "ada", avatarUrl: null, caption: "Orbit Wrapped", stats: [{ label: "Commits", value: "1,687" }] },
    { key: "card", label: "Share card" },
  ),
];

export function MyWrapped() {
  const [config, setConfig] = useState(DEFAULT_WRAPPED_CONFIG);
  return <WrappedView subject={subject} seed="ada-2026" scenes={scenes} config={config} onConfigChange={setConfig} />;
}

That renders the player, the timeline and the settings panel. A few things worth knowing:

  • A chart is data. chart: { type: "spark", values } is the whole chart. The scene draws it and plays its entrance.
  • Text takes two marks. *accent* turns words the palette's accent colour, **bold** makes them bold.
  • The seed is the video's luck. It picks the transition after each scene, so the same seed always plays the same video.
  • config is everything a viewer can change: palette, format, speed, scene order, rewritten texts. Keep it in state, or in the URL so the link is the video.
  • share and opening are optional. share adds the download button and the cards to keep. opening adds the year scan that finds hidden scenes.

The full example plays a whole made-up year with all of it switched on.

Platinum sponsor banner

Platinum sponsors

Packages

Eight packages, released together under one version number. Two packages with the same version always fit each other.

Package What is in it Version Downloads
@react-wrapped/kit Everything below in one install, plus WrappedView, the whole page npm downloads
@react-wrapped/core The stage, the motion kit, palettes, formats and transitions npm downloads
@react-wrapped/charts 21 charts and their entrances npm downloads
@react-wrapped/scenes 11 ready-made scenes and the pixel art set npm downloads
@react-wrapped/player The player, the timeline, the scene editor and the settings panel npm downloads
@react-wrapped/scan The year scan that finds hidden scenes npm downloads
@react-wrapped/graphics The share card, insight cards and the gallery under the video npm downloads
@react-wrapped/marks The marks pressed into the bottom right corner of the stage, as data npm downloads

Start with @react-wrapped/kit. Reach for the single packages when you only want a part, for example charts on a stage with no player around them:

pnpm add @react-wrapped/core @react-wrapped/charts gsap

What you can build

Every piece below plays live on the designs page, in any palette and format.

21 charts

A chart is a plain object with a type and its numbers. Each one plays in the order it is read: the structure first, the numbers next, the point last. All charts:

Spark 路 Bars 路 Funnel 路 Pulse 路 Rings 路 Donut 路 Days 路 Dial 路 Meter 路 Pixel area 路 Wheel 路 Day clock 路 River 路 Dot share 路 Then and now 路 Bubbles 路 Month up close 路 Radar 路 Where you stand 路 Mirror 路 Week by hour

19 transitions

How one scene hands over to the next. Fourteen cover the frame and clear onto the next scene, five move the scene itself. The seed picks one per scene, or you do. All transitions:

Layers 路 Slash 路 Arrow 路 Wave 路 Drip 路 Bloom 路 Pulse 路 Blink 路 Spin 路 Fan 路 Doors 路 Brush 路 Scan 路 Flash 路 Iris 路 Wipe 路 Lift 路 Drop 路 Zoom

And the rest

Part What you get Where
11 ready-made scenes Opening, big number, big day, bars, chart, ranked list, split, fact, persona, trophy case and the share card Designs
Your own scenes defineScene({ render, animate }) with the same motion kit the built-in scenes use Recipes
Player Play, pause, replay, full screen, and a timeline where a viewer hides, reorders and rewrites scenes Full example
Year scan A sweep over the year grid that finds hidden scenes and adds them to the video Full example
Share cards The year on one card, plus an insight card per finding, drawn in the browser and ready to download Full example
Three formats 9:16, 1:1 and 16:9. Scenes are laid out once and fitted to each frame Formats
Ten palettes Each in dark and light, plus a palette built from your own accent colour. All of it is CSS variables Theming
Video file The download button draws every frame at full size in the browser and saves the result. No server, no screen capture Full example

Gold sponsor banner

Gold sponsors

Recipes

Write your own scene

defineScene takes a render and an animate. The animation brings the scene in and holds it. The player adds the way out, so a custom scene works with every transition.

import { gsap } from "gsap";
import { defineScene, hold, Lines, linesIn, riseIn } from "@react-wrapped/core";

const quoteScene = defineScene<{ title: readonly string[]; quote: string }>({
  align: "center",
  render: ({ title, quote }) => (
    <>
      <Lines lines={title} />
      <p className="wr-lede wr-rise">{quote}</p>
    </>
  ),
  animate: ({ root }) => {
    const tl = gsap.timeline();
    linesIn(tl, root, { at: 0 });
    riseIn(tl, root.querySelectorAll(".wr-rise"), { at: 0.5 });
    return hold(tl, 1.5);
  },
});

const scene = quoteScene(
  { title: ["Commit message", "*of the year.*"], quote: "fix: it works now, do not ask" },
  { key: "quote", label: "Quote" },
);

Query inside root, never outside it, and build everything on the timeline you return. That is what lets the player scrub the scene and the export draw it frame by frame.

Pick a palette

The palette and the background mode live in the config. Start from the defaults and change what you need.

import { useState } from "react";
import { DEFAULT_WRAPPED_CONFIG, type WrappedConfig } from "@react-wrapped/core";

const [config, setConfig] = useState<WrappedConfig>({ ...DEFAULT_WRAPPED_CONFIG, palette: "grape", mode: "light" });

Pick a transition per scene

cuts maps a scene key to the transition that takes it out. Scenes you leave out keep the one the seed picked.

const config: WrappedConfig = { ...DEFAULT_WRAPPED_CONFIG, cuts: { total: "drip", launch: "iris" } };

Add a transition of your own

defineTransition puts yours next to the nineteen built-in ones, in the config and in the scene editor's list. A cover is three layers and three states; coverCut plays them the way every built-in cover plays.

import { coverCut, defineTransition } from "@react-wrapped/core";

// Once, where your app starts, before a player is on the page.
defineTransition({
  id: "curtain",
  label: "Curtain",
  cover: (
    <>
      <i />
      <i />
      <i />
    </>
  ),
  play: (tl, { root, group }) => {
    if (group)
      coverCut(
        tl,
        { root, group },
        { hidden: { yPercent: -101 }, covering: { yPercent: 0 }, cleared: { yPercent: 101 } },
      );
  },
});

Translate it, or make it sound like you

Every word the library shows on its own is a label. Wrap your page in WrappedProvider and replace the ones you want. A sentence with a number in it is a function, so a language with three plural forms can have all three.

import { WrappedProvider } from "@react-wrapped/core";

<WrappedProvider
  locale="pl-PL"
  labels={{
    player: { play: "Odtw贸rz", pause: "Pauza" },
    scan: { start: "Skanuj m贸j rok", findAll: "Znajd藕 wszystkie" },
  }}
>
  <MyWrapped />
</WrappedProvider>;

Recolour the controls

The video's palette belongs to the viewer. The controls around it read a few variables of their own, which you can set on any element above them. Dark values apply under a .dark class.

body {
  --wr-ui-primary: #7c3aed;
  --wr-ui-radius: 0.5rem;
  --wr-font: "Space Grotesk", sans-serif;
}

Restyle a part

Every part of the controls is a slot with a name. It carries the name as data-wr-slot, and takes a class for it through classNames, on one component or on WrappedProvider for the whole page.

<WrappedPlayer
  {...video}
  classNames={{
    "player-play": "my-play-button",
    "timeline-segment-bar": "my-bar",
  }}
/>

With Tailwind, import the stylesheet into a layer before utilities and your utilities win over the library's classes:

@import "tailwindcss";
@import "@react-wrapped/kit/styles.css" layer(components);

The styling guide lists every slot.

Silver sponsor banner

Silver sponsors

Try other BetterTyped projects

react-wrapped is cut from Hype Stack, the full-stack template I build. Here is what else I maintain.

Build your SaaS on Hype Stack

Connect any API with Hyper Fetch

Pinch. Pan. Zoom. react-zoom-pan-pinch makes any component zoomable.

Development

pnpm install
pnpm build
pnpm typecheck
pnpm lint
pnpm test

Node 24, pnpm 12, Nx, Vite 8, tsgo, oxlint and oxfmt. DESIGN.md explains how the packages fit together and the rules a change has to keep. Commits follow conventional commits, because the next version number is worked out from them.

Our sponsors

Sponsors

GSAP

GSAP is a peer dependency. You install it yourself and it comes under its own license, which is free to use, commercial projects included, but is not MIT. Read the GSAP standard license before you ship.

License

MIT

About

馃巻聽Year in review videos in React. Animated scenes, 21 charts, 19 transitions, a player with a timeline editor, a year scan and share cards, all on one GSAP timeline. The engine behind Hype Stack's Wrapped.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages