WebberUI

Card Deck

A draggable stacked card deck — flick a card out in any of four directions, the next one fills in automatically, with infinite loop or draw-until-empty modes.

Loading preview…
npx shadcn@latest add https://webberui.com/r/card-deck.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

3
96
500
<CardDeck />

Installation

npx shadcn@latest add https://webberui.com/r/card-deck.json

Or, once registries are configured in components.json, install it as @webberui/card-deck.

Usage

Each direct child is one card, and the first one starts on top. The container needs an explicit size (the cards stack on top of each other with absolute positioning).

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

<div className="h-72 w-64">
  <CardDeck className="h-full w-full">
    <div className="flex h-full items-center justify-center">Card one</div>
    <div className="flex h-full items-center justify-center">Card two</div>
    <div className="flex h-full items-center justify-center">Card three</div>
  </CardDeck>
</div>

Drawing programmatically

Grab draw() / reset() through a ref so a button can trigger a draw alongside the arrow keys and dragging.

import { CardDeck, type CardDeckHandle } from "@/components/ui/card-deck";

const deckRef = React.useRef<CardDeckHandle>(null);

<CardDeck ref={deckRef}>{/* ... */}</CardDeck>
<button onClick={() => deckRef.current?.draw("left")}>Draw</button>

Draw until empty

With loop={false}, cards are drawn away one by one and emptyState shows once the deck runs out.

<CardDeck loop={false} emptyState={<p>Nothing left</p>}>
  {/* ... */}
</CardDeck>

Props

PropTypeDefaultDescription
childrenReact.ReactNodeEach direct child is one card; the first one starts on top
indexnumberControlled index of the top card
defaultIndexnumber0Initial top-card index in uncontrolled mode
onIndexChange(index: number) => voidFires when the top card changes
onDraw(index: number, direction: DrawDirection) => voidFires when a card is drawn away, receiving the drawn card's index and direction
loopbooleantruetrue cycles a drawn card back to the bottom of the deck; false shows the empty state once the deck runs out
visibleCountnumber3Number of layers rendered in the stack at once (including the top card)
drawThresholdnumber96Drag displacement threshold that triggers a draw (px)
velocityThresholdnumber500Flick velocity threshold that triggers a draw (px/s)
emptyStateReact.ReactNodeContent shown once the deck runs out with loop={false}
cardClassNamestringStyles applied to each card's surface
classNamestringApplied to the deck container

DrawDirection is "up" | "down" | "left" | "right".

The ref receives a CardDeckHandle: draw(direction?) draws the top card away (upward by default), and reset() returns to the first card.

How it works

  • The top card can be dragged freely in all four directions; once the displacement or flick velocity crosses the threshold it flies out that way, and below the threshold it springs back into place.
  • While dragging, the card tilts slightly in proportion to the horizontal displacement; the cards behind take stack angles hashed stably from their index, so the deck looks like a naturally piled stack.
  • After a draw, the next card springs into the front automatically and a new card joins the back of the deck; with loop on, the drawn card cycles back to the bottom.
  • Supports controlled and uncontrolled modes (pass index + onIndexChange, or defaultIndex); when the number of children changes, the uncontrolled index is clamped back into the valid range automatically.

Accessibility

  • The top card is focusable (tabIndex), arrow keys map to the four draw directions, and Enter / Space draws it upward.
  • The deck container carries role="group" and aria-roledescription, with an aria-live region announcing the current card number and how many remain.
  • When the user has "reduce motion" enabled at the system level, dragging and the fly-out offset are disabled and a draw becomes a fade out, avoiding large movements.

On this page