Skip to content
react-wrapped

Insight cards

An insight card is one thing a year turned out to be: the number, the chart that shows it, and the line that says what it means. InsightCard is the card as a React component, and renderInsightCard is the same card as a PNG. Make one per finding, so a viewer has more than one picture worth posting.

Long HaulEpic
55days in a row without missing one

Contributions per week. The lit stretch is your streak: not one day missed.

@ada ยท Orbit Wrapped, Oct 2025 to Oct 2026

The component on the page, scaled to fit. The button saves it as a PNG.

Installation

bash
pnpm add @react-wrapped/graphics @react-wrapped/core gsap
tsx
import "@react-wrapped/core/styles.css";
import "@react-wrapped/graphics/styles.css";

Usage

InsightCard always draws at 1200 by 630. To show it on a page, scale it into a box of that shape. To save it, call renderInsightCard with the same spec and palette.

insight-card.tsx
import { type CSSProperties, useLayoutEffect, useRef, useState } from "react";

import { resolvePalette, type WrappedConfig } from "@react-wrapped/core";
import { InsightCard, type InsightCardSpec, renderInsightCard } from "@react-wrapped/graphics";

// The card is always drawn 1200 by 630. On a page it is scaled down to the room it has.
const BOX: CSSProperties = { position: "relative", aspectRatio: "1200 / 630", overflow: "hidden", borderRadius: 14 };

export function InsightCardOnThePage({
  spec,
  config,
  fileName,
}: {
  spec: InsightCardSpec;
  config: WrappedConfig;
  fileName: string;
}) {
  const box = useRef<HTMLDivElement>(null);
  const [scale, setScale] = useState(0.5);
  const [busy, setBusy] = useState(false);
  const palette = resolvePalette(config);

  useLayoutEffect(() => {
    const element = box.current;
    if (!element) return;
    const fit = () => setScale(element.clientWidth / 1200);
    fit();
    const observer = new ResizeObserver(fit);
    observer.observe(element);
    return () => observer.disconnect();
  }, []);

  const download = async () => {
    setBusy(true);
    try {
      // The same card, captured off screen at twice its size.
      const png = await renderInsightCard(spec, palette);
      const url = URL.createObjectURL(png);
      const link = document.createElement("a");
      link.href = url;
      link.download = fileName;
      link.click();
      setTimeout(() => URL.revokeObjectURL(url), 1000);
    } finally {
      setBusy(false);
    }
  };

  return (
    <div>
      <div ref={box} style={BOX}>
        <div style={{ transform: `scale(${scale})`, transformOrigin: "0 0" }}>
          <InsightCard spec={spec} palette={palette} />
        </div>
      </div>
      <div role="group" aria-label="Insight card">
        <button type="button" disabled={busy} onClick={() => void download()}>
          {busy ? "Drawing the PNG" : "Download the PNG"}
        </button>
      </div>
    </div>
  );
}

The spec

KeyTypeWhat it is
handlestringWritten as @handle above the chart.
captionstringThe line after the handle: what this Wrapped is called and the period.
namestringWhat the card is called: an achievement's name, or the part of the year it shows.
rarity?"common" | "rare" | "epic" | "legendary"For an achievement. Its word goes under the name and its colour lights the chart's point.
stat{ value: string; label: string }The headline number and what it counts, read as one sentence.
insightstringOne line saying what the chart shows.
chartSceneChartAny of the charts, as data. Use the one the video plays for the same finding.
brand?stringA quiet line under the chart: a site address or a product name.
ts
import type { InsightCardSpec } from "@react-wrapped/graphics";

export const fridays: InsightCardSpec = {
  handle: "ada",
  caption: "Orbit Wrapped, Oct 2025 to Oct 2026",
  name: "Friday Shipper",
  rarity: "rare",
  stat: { value: "31%", label: "of your work landed on a Friday" },
  insight: "Contributions by weekday. The lit column is Friday.",
  chart: {
    type: "bars",
    bars: [
      { label: "M", value: 312 },
      { label: "T", value: 341 },
      { label: "W", value: 330 },
      { label: "T", value: 352 },
      { label: "F", value: 398, peak: true },
      { label: "S", value: 84 },
      { label: "S", value: 61 },
    ],
  },
};

Good to know

  • The number is sized to its length: "31%" is set bigger than "12,480". Keep stat.value short.
  • With a rarity the chart's point takes that rarity's colour. A common one, or a card with no rarity, lights it in the card's own ink.
  • renderInsightCard lays the card out off screen and captures it at twice its size. Captures run one at a time, so asking for ten at once does not freeze the page.
  • The chart on a card is drawn finished. Entrances belong to the video.

Last updated . Read this page as Markdown.