Skip to content
SpatialGenerated from the public API

CardStack

Cards in depth that can be advanced, with the controls exposed as a real group.

@luc/ui/motion

Live 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.

1 / 3

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.

1 / 2

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.

1 / 5

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

Exported symbols
ExportKindSignature
CardStack11 documented props({ className, children, index, defaultIndex, onIndexChange, visible, offsetX, offsetY, shrink, spread, dim, loop, label, ...props }: CardStackProps) => import("@types/react").JSX.Element
CardStackControls2 documented props({ className, previousLabel, nextLabel, children, ...props }: CardStackControlsProps) => import("@types/react").JSX.Element
CardStackItemexport({ className, children, ...props }: React.ComponentPropsWithRef<"div">) => import("@types/react").JSX.Element
useCardStackexport() => CardStackContextValue

CardStack props

Type: CardStackProps

Values the compiler reads from the declaration.

CardStack props
PropTypeDefaultNotes
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.

CardStackControls props
PropTypeDefaultNotes
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.

Luc UI v1.0CardStackdark theme