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.
npx shadcn@latest add https://webberui.com/r/card-deck.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<CardDeck />
Installation
npx shadcn@latest add https://webberui.com/r/card-deck.jsonOr, 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
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | Each direct child is one card; the first one starts on top |
index | number | — | Controlled index of the top card |
defaultIndex | number | 0 | Initial top-card index in uncontrolled mode |
onIndexChange | (index: number) => void | — | Fires when the top card changes |
onDraw | (index: number, direction: DrawDirection) => void | — | Fires when a card is drawn away, receiving the drawn card's index and direction |
loop | boolean | true | true cycles a drawn card back to the bottom of the deck; false shows the empty state once the deck runs out |
visibleCount | number | 3 | Number of layers rendered in the stack at once (including the top card) |
drawThreshold | number | 96 | Drag displacement threshold that triggers a draw (px) |
velocityThreshold | number | 500 | Flick velocity threshold that triggers a draw (px/s) |
emptyState | React.ReactNode | — | Content shown once the deck runs out with loop={false} |
cardClassName | string | — | Styles applied to each card's surface |
className | string | — | Applied 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
loopon, the drawn card cycles back to the bottom. - Supports controlled and uncontrolled modes (pass
index+onIndexChange, ordefaultIndex); 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"andaria-roledescription, with anaria-liveregion 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.
Morph Expand Card
Click a card to morph it seamlessly into a centered detail panel via shared layout; only one card in a group is expanded at a time.
Spotlight Card
A cursor spotlight card — a border glow and a fainter background bloom track the cursor together and fade out on leave, with each card in a row staying independent.