CardStack
Cards in depth that can be advanced, with the controls exposed as a real group.
@luc/ui/motionLive preview
Arrow keys and the buttons move the deck; the layers behind the active card recede on scale, offset and light, and one readout beside the controls says where you are.
Baseline
Prepared fixture
<CardStack visible={3} offsetY={20} shrink={0.05} className="h-48 w-full max-w-sm">
{cards.map((card) => (
<CardStackItem key={card.id}>
{/* Content at the top: the bottom strip is the peek the layers behind
leave visible, so text there would ladder down the deck. */}
<Card className="flex h-32 flex-col justify-start gap-1 p-4">{card.node}</Card>
</CardStackItem>
))}
<CardStackControls>
<Position /> {/* const { index, count } = useCardStack(); */}
</CardStackControls>
</CardStack>
/* One position readout for the deck. Three copies — one inside every card —
land a line apart and are unreadable, which is why the counter lives here. */Other configurations
The same primitive, arranged differently. Each snippet is printed from the props its preview above was rendered with.
Two cards, a wider stage
A two-card deck with a wider peek, so the layer behind is legible enough to compare against the front.
Baseline
Prepared fixture
<CardStack visible={2} offsetY={24} shrink={0.07} className="h-44 w-full max-w-md">
{cards.map((card) => (
<CardStackItem key={card.id}>
<Card className="flex h-28 flex-col justify-start gap-1 p-4">{card.label}</Card>
</CardStackItem>
))}
<CardStackControls>
<Position />
</CardStackControls>
</CardStack>Five cards, a deep deck
Five positions behind three visible layers: the deck stays readable while the depth still reads as depth.
Baseline
Prepared fixture
<CardStack visible={3} offsetY={14} shrink={0.035} className="h-52 w-full max-w-sm">
{cards.map((card) => (
<CardStackItem key={card.id}>
<Card className="flex h-32 flex-col justify-start gap-1 p-4">{card.label}</Card>
</CardStackItem>
))}
<CardStackControls>
<Position />
</CardStackControls>
</CardStack>Usage
import { CardStack } from "@luc/ui/motion";<CardStack visible={3} offsetY={20} shrink={0.05} className="h-48 w-full max-w-sm">
{cards.map((card) => (
<CardStackItem key={card.id}>
{/* Content at the top: the bottom strip is the peek the layers behind
leave visible, so text there would ladder down the deck. */}
<Card className="flex h-32 flex-col justify-start gap-1 p-4">{card.node}</Card>
</CardStackItem>
))}
<CardStackControls>
<Position /> {/* const { index, count } = useCardStack(); */}
</CardStackControls>
</CardStack>
/* One position readout for the deck. Three copies — one inside every card —
land a line apart and are unreadable, which is why the counter lives here. */API
| Export | Kind | Signature |
|---|---|---|
CardStack | 11 documented props | ({ className, children, index, defaultIndex, onIndexChange, visible, offsetX, offsetY, shrink, spread, dim, loop, label, ...props }: CardStackProps) => import("@types/react").JSX.Element |
CardStackControls | 2 documented props | ({ className, previousLabel, nextLabel, children, ...props }: CardStackControlsProps) => import("@types/react").JSX.Element |
CardStackItem | export | ({ className, children, ...props }: React.ComponentPropsWithRef<"div">) => import("@types/react").JSX.Element |
useCardStack | export | () => CardStackContextValue |
CardStack props
Type: CardStackProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
defaultIndex? | number | undefined | — | |
dim? | number | undefined | — | Opacity lost per layer behind the active card. Without it every layer is drawn at full opacity, and a deck whose cards have transparent or semi-transparent surfaces becomes unread… |
index? | number | undefined | — | Controlled position. Pair with `onIndexChange`. |
label? | string | undefined | — | Accessible name for the group. |
loop? | boolean | undefined | — | Wrap past the ends instead of stopping. |
offsetX? | number | undefined | — | Horizontal offset per layer, in px. Negative fans to the left. |
offsetY? | number | undefined | — | Vertical offset per layer, in px. Positive sinks the deck downwards. |
onIndexChange? | ((index: number) => void) | undefined | — | |
shrink? | number | undefined | — | Scale lost per layer. `0.04` reads as a deck; `0` reads as a strip. |
spread? | number | undefined | — | Rotation per layer, in degrees — a small value fans the deck. |
visible? | number | undefined | — | How many cards stay visible behind the active one. |
CardStackControls props
Type: CardStackControlsProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
nextLabel? | string | undefined | — | |
previousLabel? | string | undefined | — | Accessible names for the two buttons. |
Accessibility
The accessibility contract is the one the primitive underneath the component already honours — the platform element for a control, or the ARIA pattern the component implements — and the Showcase's own pages are scanned for violations with axe.
What happens on the server, and when hydration changes it.
Family
Spatial
Depth, layering and geometry — motion that reads as physical space.
Related