Skip to content
WANCRAFT

H.313D & Experimental

3D Card Stack

A perspective card stack you can swipe, drag, click or arrow through — with spring physics.

ReactMotionTailwindintermediate

Overview

3D Card Stack layers cards in perspective — each one further back is offset, scaled and rotated in alternating directions. Drag or swipe the front card past a threshold (or flick it) to send it to the back; it tilts as you drag like a card held at its base. Navigation also works with previous/next buttons and the arrow keys on the focused stack, with an optional loop. Cards behind the front one are inert and hidden from assistive technology, and the current position is announced politely (“Card 2 of 5: Restraint”). Pass items as { id, content } or simply as children.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/card-stack.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/card-stack.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/card-stack.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
1 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { CardStack, type CardStackItem } from "@/components/ui/card-stack";

export function CardStackExample() {
  const items: CardStackItem[] = [
    { id: "one", label: "Precision", content: <div className="p-6 text-xl font-semibold">Precision</div> },
    { id: "two", label: "Restraint", content: <div className="p-6 text-xl font-semibold">Restraint</div> },
    { id: "three", label: "Intent", content: <div className="p-6 text-xl font-semibold">Intent</div> },
  ];

  return (
    <div className="w-full max-w-xs">
      <CardStack items={items} label="Principles" />
    </div>
  );
}

03Props

PropTypeDefaultDescription
items{ id: string | number; content: ReactNode; label?: string }[]—Cards to show. label is announced with the position.
childrenReactNode—Alternative to items: each child becomes a card.
spacingnumber14Vertical offset between stacked cards in px.
rotationnumber3Degrees of rotation per card of depth, alternating direction.
scaleStepnumber0.05Scale reduction per card of depth.
visiblenumber3Cards peeking out behind the front card.
loopbooleantrueWrap around at the ends.
spring{ stiffness: number; damping: number; mass: number }{ stiffness: 320, damping: 30, mass: 0.9 }Spring for every card transition.
thresholdnumber90Drag distance in px to change card; flicks faster than 650px/s also count.
showControlsbooleantruePrevious/next buttons, counter and progress dots.
accentstring"#FF7448"Focus ring and active progress dot.
labelstring"Cards"Accessible name of the carousel.
defaultIndexnumber0Initially visible card.
onIndexChange(index: number) => void—Called after the front card changes.
cardClassNamestring""Extra classes for every card surface.

04Features

  • 01Perspective stack with offset, scale and alternating rotation
  • 02Drag / swipe with distance threshold and flick velocity
  • 03Drag tilt pivoting near the card's base
  • 04Optional looping, buttons, counter and progress dots
  • 05items API or plain children
motion · springinput · draginput · clickinput · keyboard

05Accessibility

  • The stack is a labelled carousel (aria-roledescription); each card is a group named “Card n of total”.
  • Cards behind the front one are inert and aria-hidden, so their links and buttons cannot be focused.
  • The stack is focusable: Left/Right move, Home/End jump; native buttons labelled “Previous card” / “Next card” are also provided.
  • Position changes are announced via a polite live region (only after the first interaction).
  • With prefers-reduced-motion, transitions are instant and the drag tilt is disabled.