ComponentsCardStack
CardStack
Let users browse cards arranged in a stack or fan.
On this page
Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.
View registry filesImport the component into your page or component file.
import { Card, CardStack } from "@dethink/components";Try the examples, then open the code to use them in your app. Focus a stack and use Arrow, Home, and End keys to navigate.
Stacked deck
The default stack mode layers cards behind the active one and shows previous/next controls automatically when there is more than one card.
Show sourceexamples/card-stack/basic.tsx
"use client";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardStack,
CardTitle,
} from "@dethink/components";
const releases = [
{
version: "v1.4.0",
date: "June 2026",
summary:
"Date suite: Calendar, DatePicker, DateRangePicker, DateTimePicker.",
},
{
version: "v1.3.0",
date: "May 2026",
summary: "DataTable with sorting, filtering, and row selection.",
},
{
version: "v1.2.0",
date: "April 2026",
summary: "Overlay primitives: Popover, Tooltip, and DropdownMenu.",
},
];
export function CardStackBasic() {
return (
<CardStack aria-label="Latest releases">
{releases.map((release) => (
<Card key={release.version}>
<CardHeader>
<CardTitle>{release.version}</CardTitle>
<CardDescription>{release.date}</CardDescription>
</CardHeader>
<CardContent className="text-muted-foreground text-sm">
{release.summary}
</CardContent>
</Card>
))}
</CardStack>
);
}Open fan
Open mode spreads the deck into an arc — angle controls the per-card rotation, and clicking any visible card brings it to the front.
Show sourceexamples/card-stack/open.tsx
"use client";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardStack,
CardTitle,
} from "@dethink/components";
const plans = [
{ name: "Starter", price: "$0", blurb: "Two projects, community support." },
{ name: "Team", price: "$29", blurb: "Unlimited projects, shared themes." },
{ name: "Scale", price: "$99", blurb: "SSO, audit log, priority support." },
];
export function CardStackOpen() {
return (
<CardStack
aria-label="Pricing plans"
mode="open"
angle={10}
defaultActiveIndex={1}
>
{plans.map((plan) => (
<Card key={plan.name}>
<CardHeader>
<CardTitle>{plan.name}</CardTitle>
<CardDescription>{plan.price} per month</CardDescription>
</CardHeader>
<CardContent className="text-muted-foreground text-sm">
{plan.blurb}
</CardContent>
</Card>
))}
</CardStack>
);
}Boundaries and custom labels
With loop disabled the controls disable at either end, and previousLabel/nextLabel rename them for assistive technology. stackOffset deepens the deck.
Show sourceexamples/card-stack/boundaries.tsx
"use client";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardStack,
CardTitle,
} from "@dethink/components";
const steps = [
{ title: "1. Install", body: "npx shadcn@latest add @dethink/card-stack" },
{ title: "2. Compose", body: "Pass Card children straight into CardStack." },
{ title: "3. Ship", body: "Theme everything through the CSS variables." },
];
export function CardStackBoundaries() {
return (
<CardStack
aria-label="Getting started steps"
loop={false}
stackOffset={14}
previousLabel="Previous step"
nextLabel="Next step"
>
{steps.map((step) => (
<Card key={step.title}>
<CardHeader>
<CardTitle>{step.title}</CardTitle>
<CardDescription>Setup walkthrough</CardDescription>
</CardHeader>
<CardContent className="text-muted-foreground font-mono text-sm">
{step.body}
</CardContent>
</Card>
))}
</CardStack>
);
}Controlled
Drive the stack from external state with activeIndex and onActiveIndexChange — here a row of index buttons doubles as a pagination indicator.
Show sourceexamples/card-stack/controlled.tsx
"use client";
import { useState } from "react";
import {
Button,
Card,
CardContent,
CardDescription,
CardHeader,
CardStack,
CardTitle,
} from "@dethink/components";
const quotes = [
{
author: "Dana, platform lead",
quote: "We rethemed the whole library in an afternoon.",
},
{
author: "Miguel, frontend dev",
quote: "Open code means no more fighting a black box.",
},
{
author: "Priya, design systems",
quote: "The tokens map one-to-one onto our brand.",
},
];
export function CardStackControlled() {
const [activeIndex, setActiveIndex] = useState(0);
return (
<div className="space-y-4">
<CardStack
aria-label="Customer quotes"
mode="open"
activeIndex={activeIndex}
onActiveIndexChange={setActiveIndex}
>
{quotes.map((entry) => (
<Card key={entry.author}>
<CardHeader>
<CardTitle>“{entry.quote}”</CardTitle>
<CardDescription>{entry.author}</CardDescription>
</CardHeader>
<CardContent className="text-muted-foreground text-sm">
Quote {quotes.indexOf(entry) + 1} of {quotes.length}
</CardContent>
</Card>
))}
</CardStack>
<div className="flex justify-center gap-2">
{quotes.map((entry, index) => (
<Button
key={entry.author}
size="sm"
variant={index === activeIndex ? "solid" : "outline"}
onClick={() => setActiveIndex(index)}
>
{index + 1}
</Button>
))}
</div>
</div>
);
}Hidden controls
Hide the built-in icon buttons when another element drives navigation. showControls={false} hides both, while showPreviousControl/showNextControl hide just one side — here the back chevron is hidden and custom buttons advance the deck.
CardStack renders a div and accepts all native div attributes. Inactive cards are inert and hidden from assistive technology; only the active card is interactive.
| Prop | What it does | Default |
|---|---|---|
childrenCard element | Card element[] | Direct Card children only. Anything else throws, so wrappers must merge onto a Card via asChild. | Not set |
mode"stack" | "open" | Layered deck with the active card on top, or a fanned arc that spreads the cards apart. | "stack" |
activeIndexnumber | Controlled index of the active card. Pair with onActiveIndexChange. | Not set |
defaultActiveIndexnumber | Initial active card for uncontrolled usage. | 0 |
onActiveIndexChange(index: number) => void | Fires with the normalized index whenever navigation changes the active card. | Not set |
loopboolean | Wrap from the last card back to the first. When false, controls disable at the ends. | true |
showControlsboolean | Force the built-in previous/next icon buttons on or off in either mode. | stack mode with 2+ cards |
showPreviousControlboolean | Show or hide only the previous (back) icon button. Overrides showControls for that side. | showControls |
showNextControlboolean | Show or hide only the next icon button. Overrides showControls for that side. | showControls |
previousLabelstring | Accessible label for the built-in previous control. | "Show previous card" |
nextLabelstring | Accessible label for the built-in next control. | "Show next card" |
anglenumber | Per-card rotation in open mode, clamped between 0 and 30 degrees. | 15 |
stackOffsetnumber | Pixel offset applied per depth level in stack mode, clamped between 0 and 32. | 8 |
aria-label / aria-labelledbystring | Accessible name of the focusable group. Always pass one that describes the content. | "Card stack" |