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.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<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.jsonOr, 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
| Prop | Type | Default | Description |
|---|---|---|---|
cards | KeystrokeCard[] | — | Cards to answer one by one, top to bottom in array order |
defaultOptions | KeystrokeCardOption[] | Approve / Later / Skip | Default 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 |
answers | KeystrokeCardAnswers | — | Controlled answer record; when omitted the component manages it internally (uncontrolled) |
defaultAnswers | KeystrokeCardAnswers | {} | Initial answer record in uncontrolled mode |
onAnswersChange | (answers: KeystrokeCardAnswers) => void | — | Called whenever the answer record changes (both answering and undoing) |
global | boolean | false | Listen for keys on the whole window, so number keys work even when the deck is not focused |
undo | boolean | true | Allow undoing the previous card with Backspace / Ctrl+Z |
visibleStack | number | 3 | How many cards are visible at once (including the top one) |
summary | ReactNode | ((answers: KeystrokeCardAnswers) => ReactNode) | "N cards handled" plus per-option counts | Content shown once every card is answered |
stiffness | number | 340 | Spring stiffness for fly-off and restacking (damping fixed at 30); higher is snappier |
accent | string | — | Accent colour; overrides --wb-keystroke-deck-accent |
label | string | "Cards to review" | Accessible name of the deck region (the Chinese build defaults to "待回應卡片", "cards awaiting a response") |
className | string | — | Forwarded to the outermost container |
KeystrokeCard
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique card identifier |
title | string | — | Question title |
body | ReactNode | — | Body content |
options | KeystrokeCardOption[] | — | Options for this card (2–4); falls back to defaultOptions |
KeystrokeCardOption
| Field | Type | Default | Description |
|---|---|---|---|
key | string | — | Option identifier, written into answers and passed to onAnswer |
label | string | — | Option label text |
direction | "left" | "up" | "right" | By position | Fly-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
cardswithout 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
globalmode keys are not intercepted while focus is in an input, textarea, select or contenteditable element; keys pressed during IME composition (isComposingor 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 viaaria-label, witharia-describedbypointing at the "press 1–3 to respond" hint - The top card is a
role="group"; options arebuttons witharia-keyshortcuts, and the undo button works with the mouse as well - Cards below the top one get
aria-hiddenandinert, so Tab and screen readers skip them - Each answer or undo is announced in an
aria-liveregion, 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
Permission Wave Grid
An admin flips one switch; SVG wires draw from it to a member grid and avatars unlock wave by wave — showing access granted to everyone at once.
Folder Filing Grid
A document filing React component: scattered document cards fly along an arc into their matching folders, which open to catch them, close with a small dip and roll their counts up. Auto-sort in one go, or file by hand with drag or keyboard, and open a folder to see what is inside.