Interaction Choreography

Flashcard Deck

A study flashcard deck React component. Tap or press Space to flip in 3D, drag or use arrow keys to sort into known and unknown, with a progress bar and an end screen showing an accuracy ring, restart and review-unknown options.

A flashcard deck for learning vocabulary or terms. Tap a card or press Space to flip it with a 3D spring; drag right or press → to mark it known, drag left or press ← to mark it unknown. Past the threshold, or with a fast enough throw, the card rotates and flies off screen while the next one rises from the preview slot. A progress bar with known / unknown counts sits underneath, and Backspace undoes the last card. When a round ends, the deck turns into a stats card with an accuracy ring, plus "restart all" and "review unknown only".

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

260
Active edited usageDefault props
<FlashcardDeck/>

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/flashcard-deck.en.json

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

Usage

import { FlashcardDeck } from "@/components/ui/flashcard-deck";

const cards = [
  { id: "persist", front: "堅持", back: "persist (v.)", hint: "keep going" },
  { id: "curious", front: "好奇", back: "curious (adj.)" },
  { id: "hesitate", front: "猶豫", back: "hesitate (v.)" },
];

<FlashcardDeck
  cards={cards}
  onResult={(id, known) => console.log(id, known ? "known" : "unknown")}
  onComplete={(results) => console.log("round results", results)}
/>

Props

PropTypeDefaultDescription
cardsFlashcardItem[]—The cards
onResult(id: string, known: boolean) => void—Fires each time a card is sorted
onComplete(results: FlashcardResult[]) => void—Fires when a round is fully sorted; results are in answer order
stiffnessnumber260Stiffness of the flip spring (damping is fixed at 24)
showProgressbooleantrueShow the bottom progress bar, position count and known / unknown tally
allowUndobooleantrueAllow stepping back one card with Backspace / U or the undo button
shufflebooleanfalseDeterministically shuffle using seed
seednumber1Shuffle seed; the same seed always yields the same order
labelsPartial<FlashcardDeckLabels>—Interface string overrides (buttons, announcements, position text)
classNamestring—Forwarded to the outermost container

FlashcardItem

FieldTypeDefaultDescription
idstring—Unique id, returned by onResult
frontReactNode—Front content
backReactNode—Back content
hintstring—Small hint under the front content

FlashcardResult

FieldTypeDefaultDescription
idstring—Card id
knownboolean—true for known, false for unknown

Color variables

VariableDefaultDescription
--wb-flashcard-deck-known#10b981Color of the known button, drag label, tally and ring
--wb-flashcard-deck-unknown#f43f5eColor of the unknown button, drag label and tally

How it works

  • Flip: the card container sets perspective: 1200px and an inner layer springs rotateY between 0° and 180°; both faces use backface-visibility: hidden.
  • Stack: two preview cards sit under the current one (scale 0.95 / 0.9, offset 8 / 16px, opacity 0.7 / 0.4); after sorting, the next card springs up from the preview slot.
  • Sorting: while dragging, the card rotates by 1/20 of its offset and "unknown" / "known" labels fade in on either side. On release, an offset over 120px or a velocity over 500px/s throws the card (300ms ease-in, rotation ending at ±20°); otherwise it springs back to the centre.
  • On undo, the previous card flies back in from the side it was thrown to, and onResult fires again when it is re-sorted.
  • "Review unknown only" on the end screen starts a new round using only the cards whose known was false; it is disabled when every card was known.
  • shuffle uses a seeded deterministic random generator, so server and browser produce the same order with no hydration mismatch. Passing a new cards array with the same set of ids does not reset progress.

Accessibility

  • The card container is focusable (tabIndex=0), with role="group", an aria-roledescription of "flashcard" and an aria-label describing the position (for example "Card 4 of 10")
  • Keyboard: Space / Enter flips, ← marks unknown, → marks known, Backspace or U undoes
  • Visible "Unknown", "Flip" and "Known" buttons mean touch and assistive technology users never have to drag; the flip button carries aria-pressed
  • The face that is not showing is aria-hidden; when the card flips, the back content is announced through an aria-live="polite" region, and after sorting "Marked as known" or "Marked as unknown" is announced
  • The progress bar is a role="progressbar" with aria-valuenow and aria-valuemax
  • When the user has reduce motion enabled at the system level, the flip becomes a 200ms cross-fade between faces, throws become fade-outs, dragging and all rotation are disabled, and the progress bar and ring jump straight to their values