@lesetong/cardstack
v0.2.1
Published
Bidirectional, force-coupled card deck with spring gestures
Maintainers
Readme
@lesetong/cardstack
A fanned card deck with hover fan, spring swipe-to-flip and progress dots, built on motion. Available on npm.
Inspired by HyperOS 4
Features
- 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
Install
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.
Gestures
Drag up → next card
Drag down → previous card
Drag sideways → restore current cardThe 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.
Props
| 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? } |
Demo
Live Storybook: https://leset0ng.github.io/card-stack/
Dev
nub run dev # storybook
nub run build # library build
nub run typecheck # tsc --noEmit
nub run build-storybook # static docs site