Skip to content
react-cards-opening

Collection example

A collection is the most natural home for a card, so it makes sense to open cards there. Each one lies face down in its slot. Press it and it lifts out, turns over in the middle of the screen, and lands back in the same slot, face up. Press an opened one to look at it again.

Four slots. Open them in any order.

The code

collection.tsx
import { useCardOpening, useOpenedCards, useOpeningPhase } from "@react-cards-opening/core";
import { Card, CardOpening, type GlyphCard } from "@react-cards-opening/ui";
import { useRef } from "react";
import "@react-cards-opening/ui/styles.css";

export function Collection({ cards }: { cards: GlyphCard[] }) {
  const { opened, open } = useOpenedCards("collection");
  const hunt = useCardOpening({ cards, opened, onOpen: open });
  const phase = useOpeningPhase(hunt.opening);
  const slots = useRef(new Map<string, HTMLElement>());
  const slotOf = (card: GlyphCard) => slots.current.get(card.id) ?? null;
  // While a card is in the air, its slot is empty.
  const away = hunt.current && phase !== "idle" && phase !== "lead" ? hunt.current.id : null;

  return (
    <>
      <ul className="shelf">
        {cards.map((card) => {
          const isOpen = opened.includes(card.id);
          return (
            <li key={card.id}>
              <button
                ref={(element) => {
                  if (element) slots.current.set(card.id, element);
                }}
                // Seen through, not hidden: a hidden button could not take the focus back after a peek.
                style={{ opacity: away === card.id ? 0 : 1 }}
                disabled={hunt.running}
                aria-label={isOpen ? `Look at ${card.name} again` : "Open this card"}
                onClick={(event) => (isOpen ? hunt.peek(card.id, event.currentTarget) : hunt.start([card.id]))}
              >
                <Card card={card} faceDown={!isOpen} width="100%" />
              </button>
            </li>
          );
        })}
      </ul>
      <CardOpening opening={hunt.opening} launch={slotOf} landing={slotOf} />
    </>
  );
}
collection.css
.shelf {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 16px;
  list-style: none;
  padding: 0;
}

.shelf button {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

Why it looks like one card

  • The reveal starts at the slot's exact place and size, and the slot is see-through while its card is away. The eye reads it as the same card lifting out.
  • A Card scales everything with its width, so the revealed card and the one on the shelf are the same picture at two sizes.
  • faceDown flips to false while the slot is see-through, so the card is already face up when it lands back.

Open them all

tsx
<button onClick={() => hunt.start()} disabled={hunt.running || hunt.allOpened}>
  Open the rest ({hunt.pending.length})
</button>

start() with no arguments queues every unopened card. They open one after another, each from its own slot, and a press beside a card stops the run.

Last updated . Read this page as Markdown.