Interaction Choreography

Keystroke Card Deck

A stack of cards answered with number keys 1–3; each answered card flies off and the next slides up — keyboard-first, mouse-friendly.

Built for review queues, agent suggestions and batch confirmations — anything you decide one card at a time. The top card is shown in full while the next two peek out below it; with the deck focused, number keys 1–3 pick the matching option (or just click the button). The keycap presses down, the option highlights, then the card flies off toward the option's direction (1 up-left, 2 up, 3 up-right) and the cards behind move forward. Backspace or Ctrl+Z undoes the last card, which flies back along its original path. Once everything is answered, a summary shows the count for each option. The answer record supports controlled and uncontrolled modes.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add keystroke-card-deck.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

3
340
Active edited usageDefault props
<KeystrokeCardDeck/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/keystroke-card-deck.en.json

Or, once registries are configured in components.json, install it as @webberui/keystroke-card-deck.

Usage

import { KeystrokeCardDeck } from "@/components/ui/keystroke-card-deck";

<KeystrokeCardDeck
  cards={[
    { id: "dunning", title: "Send payment reminders to 3 customers?", body: "Three invoices are 14 days overdue." },
    { id: "report", title: "Move the weekly report to Friday 17:00?" },
  ]}
  onAnswer={(cardId, optionKey) => console.log(cardId, optionKey)}
  onComplete={(answers) => console.log("all done", answers)}
/>

Props

PropTypeDefaultDescription
cardsKeystrokeCard[]—Cards to answer one by one, top to bottom in array order
defaultOptionsKeystrokeCardOption[]Approve / Later / SkipDefault options used when a card has no options of its own
onAnswer(cardId: string, optionKey: string) => void—Called each time a card is answered
onComplete(answers: KeystrokeCardAnswers) => void—Called once every card is answered, with the full answer record
answersKeystrokeCardAnswers—Controlled answer record; when omitted the component manages it internally (uncontrolled)
defaultAnswersKeystrokeCardAnswers{}Initial answer record in uncontrolled mode
onAnswersChange(answers: KeystrokeCardAnswers) => void—Called whenever the answer record changes (both answering and undoing)
globalbooleanfalseListen for keys on the whole window, so number keys work even when the deck is not focused
undobooleantrueAllow undoing the previous card with Backspace / Ctrl+Z
visibleStacknumber3How many cards are visible at once (including the top one)
summaryReactNode | ((answers: KeystrokeCardAnswers) => ReactNode)"N cards handled" plus per-option countsContent shown once every card is answered
stiffnessnumber340Spring stiffness for fly-off and restacking (damping fixed at 30); higher is snappier
accentstring—Accent colour; overrides --wb-keystroke-deck-accent
labelstring"Cards to review"Accessible name of the deck region (the Chinese build defaults to "待回應卡片", "cards awaiting a response")
classNamestring—Forwarded to the outermost container

KeystrokeCard

FieldTypeDefaultDescription
idstring—Unique card identifier
titlestring—Question title
bodyReactNode—Body content
optionsKeystrokeCardOption[]—Options for this card (2–4); falls back to defaultOptions

KeystrokeCardOption

FieldTypeDefaultDescription
keystring—Option identifier, written into answers and passed to onAnswer
labelstring—Option label text
direction"left" | "up" | "right"By positionFly-off direction once chosen; otherwise 1 up-left, 2 up, 3 and beyond up-right

KeystrokeCardAnswers is Record<string, string> (card id to option key).

How it works

  • Timing is exposed as CSS variables: --wb-keystroke-deck-press (keycap press, default 80ms), --wb-keystroke-deck-highlight (gap between the option highlight and the fly-off, default 150ms) and --wb-keystroke-deck-accent (highlight colour, default #3b82f6).
  • The current card is the first card in cards without an answer; undo steps back through the order answers were given inside the component, and if a controlled value is changed externally it falls back to the last answered card in card order.
  • In global mode keys are not intercepted while focus is in an input, textarea, select or contenteditable element; keys pressed during IME composition (isComposing or keyCode 229) are always ignored.
  • Cards stack in a single CSS grid cell, so the container height follows the tallest card without a fixed height.

Accessibility

  • The deck is a role="region" named via aria-label, with aria-describedby pointing at the "press 1–3 to respond" hint
  • The top card is a role="group"; options are buttons with aria-keyshortcuts, and the undo button works with the mouse as well
  • Cards below the top one get aria-hidden and inert, so Tab and screen readers skip them
  • Each answer or undo is announced in an aria-live region, e.g. "Selected: Approve, 5 left"
  • When answering by clicking an option, focus is handed back to the deck container so it is not lost when the card unmounts
  • When the user has reduce motion enabled at the system level, cards simply fade out and the next one snaps into place, with no fly-off or spring restacking