Skip to content

About

πŸƒ Card opening animations for React. A card flies out, hums, flips and lands, with rarity effects from a hop to a legendary foil. For pack openings, loot drops, gacha reveals and daily rewards.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

29 Commits

Folders and files

Repository files navigation

Card opening for React. A free tool from Hype Stack.

Docs | Effects | Examples | Discord


Join our Discord community


What it is

react-cards-opening is the moment a card is opened, as a library. A card lifts out of wherever it was, flies to the middle of the screen face down, hums, turns over, stays as long as it takes to read, and lands where it belongs.

How it arrives depends on how rare it is. Six tiers ship (common, uncommon, rare, epic, legendary, mythic) and each has a show of its own: a common is set down in a puff of dust, a rare arrives in ice, an epic over a turning sigil, a legendary in a pillar of light, and a mythic puts the lights out and arrives in an impact frame with the air cracking around it.

There are five ways to get to that moment: a sealed pack that bursts and deals its cards face down, a wheel of prizes that lands on your card, a reel that races under a marker and stops on it, a case that opens and gives its cards up one by one, and a hunt that scans a grid for them. On top sits a library of 64 effects (particles, light, shockwaves, screen effects) to play on any of them, or on anything else.

It was built for the year in review inside Hype Stack, where it turns achievements into cards. The reveal started there; the pack, the reel, the case, the six tiers and the effects grew here.

A legendary card face up in the middle of the screen, with rays behind it and a foil border

Install

pnpm add @react-cards-opening/ui @react-cards-opening/core gsap

Load the stylesheet once, anywhere in your app:

import "@react-cards-opening/ui/styles.css";

You need React 18 or 19 and GSAP 3.13 or newer. Every class in the stylesheet starts with rco- and it only styles what the library renders, so there is no Tailwind to set up, no reset to load, and nothing leaks into your page.

Quick start

Describe the cards, keep track of which are opened, and give them somewhere to hide.

import { useOpenedCards } from "@react-cards-opening/core";
import { CardHunt, GLYPHS, themeOf, type GlyphCard } from "@react-cards-opening/ui";
import "@react-cards-opening/ui/styles.css";

const cards: GlyphCard[] = [
  { id: "lantern-fox", rarity: "rare", name: "Lantern Fox", line: "Knows a shortcut.", art: GLYPHS.fox },
  { id: "ember-cat", rarity: "epic", name: "Ember Cat", line: "Sleeps on warm machines.", art: GLYPHS.cat },
  {
    id: "vault-dragon",
    rarity: "legendary",
    name: "Vault Dragon",
    line: "Keeps one of every card.",
    art: GLYPHS.dragon,
  },
];

// The grid the cards hide in: one number per cell, 0 (unlit) to 4. Use your own data.
const levels = Array.from({ length: 30 * 7 }, (_unused, day) => [0, 1, 2, 0, 3, 2, 4, 1, 0, 2][(day * 7) % 10]);

export function Hunt() {
  // Remembered on this device, under a key that changes with the deal.
  const { opened, open } = useOpenedCards("deal:ada:2026");
  return <CardHunt cards={cards} opened={opened} onOpen={open} levels={levels} theme={themeOf("grape", "dark")} />;
}

That is the whole hunt: a grid, a scan button, the reveal for each card found, and a list of finds that can be opened again. Your first opening builds it up from one card and one button.

Platinum sponsor banner

Platinum sponsors

Packages

Three packages, released together under one version number. Packages with the same version always fit. ui brings effects with it, so the install line above is still the whole of it.

Package What it holds
@react-cards-opening/core The rules without a look: the queue, a pack's stages, what each press means, rarity tiers, seeded rolls, opened-card storage. React is its only dependency.
@react-cards-opening/effects 64 effects and a layer to play them on: particles, light, shockwaves, screen effects. Works on its own, without cards. GSAP inside.
@react-cards-opening/ui The look: the pack, the wheel, the reel, the case and the hunt, the reveal they share, ten themes, one stylesheet. GSAP inside.

What is in it

Part What it does Docs
CardPack A sealed pack. It bursts on a press, deals its cards face down, and each one turns over on a press, in any order CardPack
CardSpin A reel that races under a marker and stops on the card that was dealt. The strip is filled by the real odds CardSpin
CardWheel A prize wheel: wedges in their tiers' colours turn under a pointer and stop on the card that was dealt CardWheel
CardCase A case that shakes, opens, and gives up its cards one after another onto a shelf CardCase
CardHunt A grid with cards hidden in it, the scan that finds them, and the list of finds, in one component CardHunt
CardOpening The reveal they all end in. A card leaves an element you name, turns over in the middle of the screen and lands on another CardOpening
64 effects Sparks, confetti, embers, fireworks, rays, lens flares, lightning, shockwaves, novas, screen shake, glitch and more. One name each, and they stack Effects library
effects A plan on any opener: which tier plays which effects as its card turns. Six ready-made sets, or your own Effect plans
EffectLayer A sheet to play any effect on anything: a level-up, a purchase, a streak EffectLayer
Card A card standing still, at any size, for a collection or a shop. A legendary keeps its foil on CSS alone Card
ScanGrid, SpinReel, PrizeWheel, LootCase The grid, the reel, the wheel and the case on their own, for a layout that is yours Components
useCardOpening, useCardPack The rules with nothing drawn: the queue, a pack's stages, what each press means. Build your own look on them Core
rollCards Deals cards by rarity weight. With a seed, the same hand on a server and in a browser Rolls and odds
classNames A class of your own on any part. Tailwind utilities or plain CSS, no !important Styling by hand
Ten themes Each in dark and light, or a whole theme from one accent colour Theming

Gold sponsor banner

Gold sponsors

Recipes

A pack, with effects

A sealed pack of five and a plan that says what each tier plays as its card turns.

import { useOpenedCards } from "@react-cards-opening/core";
import { CardPack, EFFECT_SETS } from "@react-cards-opening/ui";

export function Pack({ cards }: { cards: GlyphCard[] }) {
  const { opened, open } = useOpenedCards("pack:42");
  return <CardPack cards={cards} opened={opened} onOpen={open} effects={EFFECT_SETS.radiant} />;
}

Swap CardPack for CardWheel, CardSpin or CardCase and the same cards open on a wheel, on a reel or out of a case.

An effect on anything

import { EffectLayer, type EffectLayerHandle } from "@react-cards-opening/ui";

const layer = useRef<EffectLayerHandle>(null);

<div style={{ position: "relative" }}>
  <EffectLayer handle={layer} />
  <button onClick={(event) => layer.current?.play(["shockwave", "confetti"], { at: event.currentTarget })}>
    Claim
  </button>
</div>;

Open a card from a button

CardOpening on its own, with a button to leave from. This is the shape of a daily reward or a loot drop.

import { rollCards, useCardOpening } from "@react-cards-opening/core";
import { CardOpening } from "@react-cards-opening/ui";
import { useRef, useState } from "react";

export function DailyCard({ userId, today }: { userId: string; today: string }) {
  const button = useRef<HTMLButtonElement>(null);
  // The same person gets the same card all day, on any device.
  const [cards] = useState(() => rollCards({ pool, count: 1, seed: `${userId}:${today}` }));
  const [opened, setOpened] = useState<string[]>([]);
  const hunt = useCardOpening({ cards, opened, onOpen: (id) => setOpened((now) => [...now, id]) });

  return (
    <>
      <button ref={button} onClick={() => hunt.start()} disabled={hunt.running || hunt.allOpened}>
        Open today's card
      </button>
      <CardOpening opening={hunt.opening} launch={() => button.current} />
    </>
  );
}

Tiers of your own

A tier says how often it comes up and how loud its reveal is. The theme says what colour it wears.

import type { RarityTier } from "@react-cards-opening/core";
import { themeOf } from "@react-cards-opening/ui";

const tiers: RarityTier[] = [
  { id: "bronze", label: "Bronze", weight: 70, effect: "quiet" },
  { id: "silver", label: "Silver", weight: 25, effect: "loud" },
  { id: "gold", label: "Gold", weight: 5, effect: "legendary" },
];
const theme = { ...themeOf("mono", "dark"), rarity: { bronze: "#d08a5a", silver: "#c3ccd6", gold: "#f2cc60" } };

<CardHunt rarities={tiers} theme={theme} {...rest} />;

Style it by hand

Every part takes a class of your own through classNames, and every rule in the stylesheet weighs one class, so yours wins. With Tailwind 4, import the stylesheet into the components layer and utilities work on any part.

@import "tailwindcss";
@import "@react-cards-opening/ui/styles.css" layer(components);
<CardHunt
  classNames={{
    root: "rounded-none bg-zinc-950 p-10",
    headline: "font-serif text-5xl",
    scanButton: "rounded-md bg-pink-500 px-8 text-white",
    face: "rounded-xl p-4",
  }}
  {...rest}
/>

Plain CSS works the same way: .my-hunt .rco-hunt-headline { ... }. The parts, the states and the variables are listed in Styling by hand.

Only the rules

core on its own has no GSAP and no stylesheet. It answers what a press means and when a card counts as opened, and you draw the rest. Your own UI on core builds a reveal from a div and CSS.

const hunt = useCardOpening({ cards, opened, onOpen });

hunt.start(); // open every unopened card, one after another
hunt.pressCard(); // face down: turn it. face up: send it home
hunt.pressBackdrop(); // stop. a turned card still lands
hunt.openAll(); // skip the show

Silver sponsor banner

Silver sponsors

Try other BetterTyped projects

react-cards-opening 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. Commits follow conventional commits, because the next version number is worked out from them. UPSTREAM.md says where the code came from and how it is kept in step.

The look is guarded by a picture test: apps/playground plays the library frame by frame and compares every frame with a stored one.

pnpm --filter @react-cards-opening/playground e2e

Our sponsors

Sponsors

GSAP

GSAP is a peer dependency of @react-cards-opening/ui. 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. @react-cards-opening/core does not use it.

License

MIT

About

πŸƒ Card opening animations for React. A card flies out, hums, flips and lands, with rarity effects from a hop to a legendary foil. For pack openings, loot drops, gacha reveals and daily rewards.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages