Skip to content
react-cards-opening

CardOpening

CardOpening is the reveal itself. It renders nothing until a card is opened, then takes the screen: the card leaves an element you name, flies to the middle face down, hums, turns over with the show its tier asked for, and shrinks into the element where it belongs.

Each card leaves its own place and lands back on it.

Usage

tsx
import { useCardOpening, useOpenedCards } from "@react-cards-opening/core";
import { CardOpening, themeOf } from "@react-cards-opening/ui";

function Shop({ cards }) {
  const { opened, open } = useOpenedCards("shop");
  const hunt = useCardOpening({ cards, opened, onOpen: open });
  const places = useRef(new Map<string, HTMLElement>());

  return (
    <>
      {cards.map((card) => (
        <button
          key={card.id}
          ref={(element) => element && places.current.set(card.id, element)}
          onClick={() => hunt.start([card.id])}
        >
          Open
        </button>
      ))}
      <CardOpening
        opening={hunt.opening}
        theme={themeOf("grape", "dark")}
        launch={(card) => places.current.get(card.id) ?? null}
        landing={(card) => places.current.get(card.id) ?? null}
      />
    </>
  );
}

Props

PropTypeWhat it does
openingOpeningThe rules, from useCardOpening.
theme?CardThemeThe colours. moss in the dark by default.
rarities?readonly RarityTier[]Your tiers. The six defaults otherwise.
launch?(card, show) => HTMLElement | CardLaunch | nullWhere the card comes out of. Asked once per card. With nothing, the card grows out of the middle of the screen.
landing?(card) => HTMLElement | nullWhere the card goes. Asked for a few frames running, since the element may not be rendered yet. With nowhere, the card fades out.
renderFace?(card, info) => ReactNodeThe front. Rows with the class rco-line rise in as it turns.
renderBack?(card) => ReactNodeThe back, instead of the question glyph.
noteOf?(card) => string | undefinedA small last line on the ready-made face.
readingOf?(card, info) => readonly string[]The text on a face of your own, so the card stays up long enough to read.
labels?Partial<CardOpeningLabels>What a screen reader and a tooltip say.
effects?EffectPlan | falseWhat each tier plays on top of the card's own reveal. The signature shows when left out, false for none.
classNames?CardOpeningClassNamesA class of your own for each part. See Styling by hand.
width?numberThe card's width in pixels. 300 by default, 78% of a narrow screen at most.
ratio?numberHeight over width. 4 / 3 by default.
container?Element | nullWhere the overlay mounts. The document's body by default.

Something to play first

launch can return more than an element. Hand back a GSAP timeline as lead and the card waits for it: a lid lifting, a pack tearing, a scan closing in. release is called when the card has landed or the run was stopped, to put things back.

tsx
<CardOpening
  opening={hunt.opening}
  launch={(card, { tone }) => {
    const box = boxRef.current;
    if (!box) return null;
    const lead = gsap.timeline();
    lead.to(box, { rotate: -6, duration: 0.08, yoyo: true, repeat: 5 });
    lead.to(box, { boxShadow: `0 0 40px 6px ${tone}`, duration: 0.3 });
    return { element: box, lead, release: () => gsap.set(box, { clearProps: "all" }) };
  }}
/>

The second argument says what is about to happen: tone is the tier's colour on the theme, plan is its show plan, and reduced is whether motion is reduced.

Presses

The card is a button and the dimmed page behind it is the backdrop. What each press does is decided by the rules: face down, a press turns the card; face up, it sends it home; a press beside it stops the run.

Looking at a card again

tsx
<button onClick={(event) => hunt.peek(card.id, event.currentTarget)}>{card.name}</button>

A peek brings an opened card up out of what was pressed, face up, with no turn. It stays until a press on it, a press beside it or Escape sends it back, and focus returns to where it came from.

Can two openings be on one page?
Yes. Each useCardOpening has its own rules and each CardOpening its own overlay. Nothing is kept on window.
Why does the card not fly to my landing element?
landing returned null for thirty frames running, or motion is reduced. Check that the element is rendered by the time the card turns over: it is asked for after onOpen fires.
Does it lock scrolling?
No. The backdrop takes presses while a card is up, and positions are measured when the card leaves and again when it lands, so a page that scrolled in between is still right.

Last updated . Read this page as Markdown.