Skip to content
Tier

Card deck

Pro

A stack of cards with one on top and the next two peeking behind it: suggestions to go through one at a time, a queue to triage. Previous and Next go round the deck, as do the arrow keys on the deck (Right is next, and Left in a right-to-left page), and each step is announced with the card's name and where it is ("Invoice 1042, 2 of 5"). With onDismiss, dragging the top card past 40% of its width, or Delete on the deck, dismisses it: the deck calls onDismiss with its key and the app takes it out of items; without it, a drag that far goes to the next card. Backspace doesn't (since 5.0.0): to most people it means "go back" or "delete a character". Once the app has taken the card out, the deck announces it with the new top card ("Invoice 1041 dismissed. Invoice 1042, 1 of 3"), once; a card still in items once the deck has the app's answer is a dismissal the app refused, and is never announced. An app that takes the card out after an await returns that promise from onDismiss, and its answer is in when the promise settles. The deck's keys are declared in aria-keyshortcuts and in a hint, read with the deck and shown while it has keyboard focus ("Delete to dismiss, Left and right arrows to move."); the hint's line is kept while it is hidden, so showing it doesn't move what is below, and a screen reader's browse mode finds it while hidden. A press becomes a drag only once the pointer has moved 6 pixels, so a click on a button or link in a card is a click. The cards behind are only a picture of the deck: they are inert and hidden from assistive technology until they reach the top. With one card, Previous and Next are locked and say why.

Markdown
Version 5.2.0· other versions

This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).

Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0, 2.9.0, 2.10.0, 3.0.0, 4.0.0, 4.0.1, 4.0.2, 4.1.0, 4.2.0 and 4.3.0: the page came later, or had another name then.

Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.

Preview

Loading the preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/card-deck

Brings in: bidi, button, locale, locked, motion, tokens, use-announcer, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

import * as React from "react"
import { CardDeck } from "@/components/candor/ui/card-deck"

type Suggestion = { id: string; title: string; why: string }

const suggestions: Suggestion[] = [
  { id: "s1", title: "Archive 14 stale records", why: "Not opened in 18 months and owned by people who have left." },
  { id: "s2", title: "Merge two duplicate accounts", why: "Same name, address and tax number." },
  { id: "s3", title: "Turn on two-step sign-in for admins", why: "3 of 5 admins sign in with a password only." },
  { id: "s4", title: "Shorten the export retention", why: "Exports are kept for 400 days; your policy says 90." },
]

// Go through suggestions one at a time. Previous and Next (or the arrow keys on the deck) go round it, and each step
// is announced. Dragging the top card past 40% of its width, or Delete on the deck, dismisses it; once the app has
// taken it out, the deck announces it with the new top card. The deck's keys show in a hint while it has keyboard focus.
export default function Example() {
  const [items, setItems] = React.useState(suggestions)
  return (
    <CardDeck
      label="Suggestions"
      className="w-full max-w-sm"
      items={items}
      keyOf={(s) => s.id}
      labelOf={(s) => s.title}
      onDismiss={(id) => setItems((list) => list.filter((s) => s.id !== id))}
      renderCard={(s) => (
        <div className="flex min-h-28 flex-col gap-1">
          <p className="font-medium">{s.title}</p>
          <p className="text-sm text-muted-foreground">{s.why}</p>
        </div>
      )}
    />
  )
}

<CardDeck>

PropTypeDefaultDescription
itemsrequiredT[]—The cards, in order.
keyOfrequired(item: T) => string—Each card's key, stable while it is in the deck.
labelOfrequired(item: T) => string—Each card's name, announced when it comes to the top. Shown as text, never as markup.
renderCardrequired(item: T) => React.ReactNode—Draws a card's face.
onDismiss((key: string) => void | PromiseLike<unknown>) | undefined—Called with the top card's key when the person dismisses it; take it out of `items` in the same update, or, when that waits on something (a save), return the promise and take it out before it resolves. A card still there once the deck has the answer (the redraw, or the promise settling) stays, and its dismissal isn't announced; so does one whose promise rejects. Left out, a card can't be dismissed.
labelrequiredstring—What the deck is: the name of its group, "Suggestions".
classNamestring | undefined—Classes for the root, merged with Candor's.

Strings

The fixed words, in English by default. Change any of them under cardDeck in <LocaleProvider strings={{ … }}>; the table is exported as cardDeckStrings.

KeyEnglish
previousPrevious
nextNext
count{n} of {total}
position{card}, {n} of {total}
dismissed{card} dismissed
dismissedNext{card} dismissed. {next}, {n} of {total}
keysMoveDismissDelete to dismiss, Left and right arrows to move.
keysMoveLeft and right arrows to move.
keysDismissDelete to dismiss.
emptyNo cards left.
onlyOneThere's only one card.

Motion

MomentFamilyWhat it does
cyclemoveWhen the person goes to the next or previous card, or one is dismissed, the cards behind move up into place and the top card goes to the back. Under reduced motion or the calm level they jump to their places. A deck drawn with its cards is drawn still.
dragclockWhile the person drags the top card, it follows the pointer, painted from the pointer itself; let go short of the threshold, it moves back into place.

Set with --cd-motion-card-deck-<moment>-* in your brand file; the families are in the design reference.