Card deck Pro

A pile of cards to throw. Pick up the top one and it turns about where you're holding it, like a real card: hold it high and it leans into the drag, low and it leans back. Let go gently and it springs back onto the pile. Throw it and it flies off at the speed you gave it, while the next one rises.

Pro preview

Installation

Terminalbash
npx @neelshha/ui@latest login
npx @neelshha/ui@latest add card-deck

Usage

page.tsxtsx
import { CardDeck } from "@/components/ui/card-deck";

<CardDeck
  aria-label="Postcards"
  onSwipe={(index, direction) => sort(index, direction)}
>
  <Postcard city="Lisbon" />
  <Postcard city="Tokyo" />
  <Postcard city="Reykjavík" />
</CardDeck>

Dragging a card nearly half its width throws it, and so does a quick flick of any length. A card dragged out and pulled back stays. Only the top card takes the pointer; the ones under it wait, fanned a little, and loosen when you reach for the pile.

Buttons

The ref's swipe and undo work the deck from buttons of your own. Undo brings a card back from the side it went.

Pro preview
page.tsxtsx
const deck = useRef<CardDeckHandle>(null);

<CardDeck ref={deck} loop aria-label="Postcards">…</CardDeck>
<button onClick={() => deck.current?.swipe("left")}>Pass</button>
<button onClick={() => deck.current?.undo()}>Undo</button>
<button onClick={() => deck.current?.swipe("right")}>Keep</button>

Running out

Without loop the deck runs out and shows empty. onSwipe and onUndo keep the tally right, undo included.

Pro preview
page.tsxtsx
<CardDeck
  aria-label="Cities to visit"
  onSwipe={(index, direction) => tally(index, direction, 1)}
  onUndo={(index, direction) => tally(index, direction, -1)}
  empty={<Summary />}
>
  {cities.map((city) => <Postcard key={city.name} city={city} />)}
</CardDeck>

Keyboard

The deck is one tab stop. The Left and Right arrows throw the top card that way, and Backspace brings the last one back. Only the top card can be reached; the rest are inert. Screen readers hear which card is on top, like "Card 2 of 5", so give the deck a name with aria-label.

Source

API

Other props go to the outer div. Cards take their size from --ns-deck-width and --ns-deck-ratio. With reduced motion a thrown card goes at once, and nothing springs.

PropTypeDefaultDescription
childrenReactNodeThe cards, top first.
onSwipe(index: number, direction: "left" | "right") => voidRuns as a card is thrown, with its place among the children and the way it went.
onUndo(index: number, direction: "left" | "right") => voidRuns as undo brings a card back, with the way it had gone.
loopbooleanfalseThrown cards tuck back under the pile instead of leaving it.
emptyReactNodeShown once every card is gone.
depthnumber3How many cards show under the top one.
refRef<{ swipe(direction), undo() }>For buttons of your own.