A fanned card deck with hover fan, spring swipe-to-flip and progress dots, built on motion. Available on npm.
Inspired by HyperOS 4
- Hover fan — cards 2/3 tilt out behind the front card with progressive blur
- Bidirectional flip — drag up for the next card or down for the previous card; horizontal-only releases restore without changing the index
- Force-coupled deck — vertical pulls give the second card a subtle same-direction follow; downward pulls also counterload the backmost card
- Progress dots — white dot indicator on the right tracks your position in the deck
- Controlled rolling — optionally animate parent-driven index jumps through every intermediate card
- Configurable — per-card
CardPose(tilt / origin / blur), size variant, radius - Fast — three cards at rest, with a short-lived maximum of five during overlapping gestures
npm install @lesetong/cardstack
# or
pnpm add @lesetong/cardstackRequires react and motion as peer dependencies.
import { CardStack } from '@lesetong/cardstack'
import '@lesetong/cardstack/style.css'
<CardStack
cards={[<img src="a.jpg" alt="" />, <img src="b.jpg" alt="" />]}
size="large"
second={{ rotate: 6 }}
third={{ rotate: -6, blur: 3 }}
/>Controlled usage:
import { useState } from 'react'
const [index, setIndex] = useState(0)
<CardStack
cards={cards}
index={index}
animateIndexChanges
indexDirection="auto"
onChange={setIndex}
/>Passing index makes the component controlled. Parent-driven index changes
synchronize immediately by default. Enable animateIndexChanges to roll through
every intermediate card. indexDirection="auto" takes the shortest circular
route; use "forward" or "backward" to force a route. Intermediate visual
steps do not call onChange. Omit index for uncontrolled usage; use
defaultIndex to choose the initial card.
Drag up → next card
Drag down → previous card
Drag sideways → restore current card
The front card follows both axes, but only projected vertical movement changes the index. During vertical pulls, the visible second card follows the same x/y direction at a much smaller amplitude. A downward pull also keeps the backmost card in its original z-index and counterloads it. Releasing a committed backward gesture promotes the backmost card and returns the dragged card to the center as the new second slot.
| Prop | Type | Default | Description |
|---|---|---|---|
cards |
ReactNode[] |
— | Card contents, front to back |
index |
number |
— | Controlled front-card index (0-based) |
defaultIndex |
number |
0 |
Initial index for uncontrolled usage |
animateIndexChanges |
boolean |
false |
Roll through intermediate cards when the parent changes index |
indexDirection |
"auto" | "forward" | "backward" |
"auto" |
Route used for animated index changes; auto chooses the shortest circular path |
onChange |
(index: number) => void |
— | Called after a user gesture successfully flips a card |
size |
"large" | "small" |
"large" |
Variant (dimensions, origins, dots) |
width / height |
number |
per size | Deck size (px) |
radius |
number |
14 / 12 |
Card corner radius (px) |
second / third |
CardPose |
— | Pose overrides: { rotate?, origin?, blur? } |
Live Storybook: https://leset0ng.github.io/card-stack/
nub run dev # storybook
nub run build # library build
nub run typecheck # tsc --noEmit
nub run build-storybook # static docs site