Skip to content
react-cards-opening

CardHunt

CardHunt is the whole experience in one component. Cards hide in a grid. A button starts a scan that finds them one by one, each with the reveal its tier calls for. Afterwards the panel stays: the finds are listed, each can be looked at again, and the scan can be run again from a blank grid.

5 cards are hiding in here.

Run the scan. Each card opens where the beam finds it.

  • Still hidden
  • Still hidden
  • Still hidden
  • Still hidden
  • Still hidden

Five cards are in there. Press the button.

Usage

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

function Hunt({ cards, activity }) {
  const { opened, open } = useOpenedCards("deal:ada:2026");
  return (
    <CardHunt
      cards={cards}
      opened={opened}
      onOpen={open}
      levels={activity} // 0 to 4, one per cell
      theme={themeOf("grape", "dark")}
      describeSpot={(cell) => `day ${cell + 1}`}
      sessionKey="deal:ada:2026:spots"
    />
  );
}

The grid

levels is the grid: one number per cell, 0 for unlit up to 4. Cells fill top to bottom, then left to right, seven to a column unless rows says otherwise. Cards hide on lit cells. A year of activity is the obvious grid, but it can be any data that makes a shape: a season of matches, a board, a month of orders.

Props

PropTypeWhat it does
cardsreadonly C[]Every card in the deal, found or not. Each needs a name.
openedreadonly string[]Ids already opened.
onOpen(id: string) => voidA card was turned face up for the first time.
levelsreadonly number[]The grid: one number per cell, 0 to 4.
lead?numberEmpty slots before the first cell, to line a calendar up with its weekday.
rows?numberCells in a column. 7 by default.
theme?CardThemeThe colours.
rarities?readonly RarityTier[]Your tiers.
describeSpot?(cell: number) => stringHow a cell reads on a card and a badge: "Mar 14".
actions?ReactNodeExtra buttons in the header, next to the scan button.
sessionKey?string | nullWhere this visit remembers each card's cell, so replays reopen the same tiles.
hero?booleanThe hunt is the page: bigger type, and it fills the box it is given.
onRunChange?(running: boolean) => voidA scan started or came to rest.
labels?Partial<CardHuntLabels>Every word in the panel.
effects?EffectPlan | falseWhat each tier plays on top of the card's own reveal. The signature shows when left out, false for none.
landing?(card) => HTMLElement | nullWhere a found card lands instead of its badge. Answer null and it fades out.
renderFace?(card, info) => ReactNodeThe front of the card.
renderBack?(card) => ReactNodeThe back of the card.
readingOf?(card, info) => readonly string[]The text on a face of your own.
className?stringAdded to the panel.
classNames?CardHuntClassNamesA class of your own for each part: the panel's, the grid's and the reveal's. See Styling by hand.
style?CSSPropertiesAdded to the panel.

As the page

With hero, the hunt is what the visitor came for. The headline is larger, and the panel fills the height of its container with the grid in the middle. Give the container a height.

2 cards are hiding in here.

Run the scan. Each card opens where the beam finds it.

  • Still hidden
  • Still hidden

What the buttons do

ButtonWhenWhat it does
ScanCards are still hiddenFinds them one after another.
Find allDuring a scan with more to comeSkips the show and opens the rest at once.
Scan againEverything is foundReplays from a blank grid. Each card comes out of the same tile.
A badge or a lit cellAfter a findBrings that card up again.

Your own words

Every line in the panel comes from labels. The headline and the subline are functions of where the hunt stands, so they can count. Labels and translation lists them all.

tsx
<CardHunt
  labels={{
    headline: ({ pending, running }) => (running ? "Looking..." : `${pending} left to find`),
    scan: () => "Search the board",
    found: (where) => `Was on ${where}`,
  }}
  {...rest}
/>

Last updated . Read this page as Markdown.