Not affiliated with official shadcn/ui. Visit ui.shadcn.com for official docs.
Unlock this block—Get Pro at 60% offReact Dialog Block Flashcard
Flashcard study dialog with flip animation, progress tracking, shuffle mode, and mastery marking
Looking to implement shadcn/ui blocks?
Join our Discord community for help from other developers.
Study smarter with interactive flashcards. This React flashcard dialog provides smooth flip animations, progress tracking through card sets, shuffle mode for randomization, and mastery marking for learned cards. Built with shadcn/ui Dialog, Button, Badge, and Progress components using Tailwind CSS, users study efficiently with visual feedback on their progress. Flip cards, mark mastered, track progress—perfect for language learning apps, study tools, quiz platforms, or any Next.js application where spaced repetition and active recall improve knowledge retention.
"use client";import { useState, useCallback } from "react";import { BookOpen, ChevronLeft, ChevronRight, Shuffle, RotateCcw, Check, X, Sparkles, Brain,} from "lucide-react";import { Button } from "@/components/ui/button";import { Badge } from "@/components/ui/badge";import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog";import { Progress } from "@/components/ui/progress";import { cn } from "@/lib/utils";type Flashcard = { id: string; front: string; back: string; mastered: boolean;};const initialCards: Flashcard[] = [ { id: "1", front: "What is React?", back: "A JavaScript library for building user interfaces", mastered: false }, { id: "2", front: "What is JSX?", back: "A syntax extension that allows writing HTML-like code in JavaScript", mastered: false }, { id: "3", front: "What is a Hook?", back: "Functions that let you use state and other React features in functional components", mastered: false }, { id: "4", front: "What is useState?", back: "A Hook that lets you add state to functional components", mastered: false }, { id: "5", front: "What is useEffect?", back: "A Hook for performing side effects in functional components", mastered: false }, { id: "6", front: "What is a Component?", back: "A reusable piece of UI that can have its own state and logic", mastered: false },];export const title = "React Dialog Block Flashcard";export default function DialogFlashcard() { const [open, setOpen] = useState(false); const [cards, setCards] = useState<Flashcard[]>(initialCards); const [currentIndex, setCurrentIndex] = useState(0); const [isFlipped, setIsFlipped] = useState(false); const [showMasteredOnly, setShowMasteredOnly] = useState(false); const activeCards = showMasteredOnly ? cards.filter((c) => c.mastered) : cards.filter((c) => !c.mastered); const currentCard = activeCards[currentIndex]; const masteredCount = cards.filter((c) => c.mastered).length; const progress = (masteredCount / cards.length) * 100; const handleFlip = () => { setIsFlipped(!isFlipped); }; const handleNext = useCallback(() => { setIsFlipped(false); setTimeout(() => { setCurrentIndex((prev) => (prev + 1) % activeCards.length); }, 150); }, [activeCards.length]); const handlePrevious = useCallback(() => { setIsFlipped(false); setTimeout(() => { setCurrentIndex((prev) => (prev - 1 + activeCards.length) % activeCards.length); }, 150); }, [activeCards.length]); const handleShuffle = () => { const shuffled = [...cards].sort(() => Math.random() - 0.5); setCards(shuffled); setCurrentIndex(0); setIsFlipped(false); }; const handleMarkMastered = (mastered: boolean) => { if (!currentCard) return; setCards((prev) => prev.map((card) => card.id === currentCard.id ? { ...card, mastered } : card ) ); // Move to next card if there are more if (activeCards.length > 1) { setIsFlipped(false); setTimeout(() => { setCurrentIndex((prev) => Math.min(prev, activeCards.length - 2)); }, 150); } }; const handleReset = () => { setCards(initialCards.map((c) => ({ ...c, mastered: false }))); setCurrentIndex(0); setIsFlipped(false); setShowMasteredOnly(false); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === " " || e.key === "Enter") { e.preventDefault(); handleFlip(); } else if (e.key === "ArrowRight") { handleNext(); } else if (e.key === "ArrowLeft") { handlePrevious(); } }; const allMastered = masteredCount === cards.length; return ( <Dialog open={open} onOpenChange={setOpen}> <div className="flex min-h-[350px] items-center justify-center"> <DialogTrigger asChild> <Button variant="outline"> <BookOpen className="mr-2 h-4 w-4" /> Study Flashcards </Button> </DialogTrigger> </div> <DialogContent className="sm:max-w-md" onKeyDown={handleKeyDown}> <DialogHeader> <DialogTitle className="flex items-center justify-between"> <div className="flex items-center gap-2"> <BookOpen className="h-5 w-5" /> Flashcards </div> <div className="flex items-center gap-2"> <Button variant="ghost" size="icon" className="h-8 w-8" onClick={handleShuffle} title="Shuffle" > <Shuffle className="h-4 w-4" /> </Button> <Button variant="ghost" size="icon" className="h-8 w-8" onClick={handleReset} title="Reset" > <RotateCcw className="h-4 w-4" /> </Button> </div> </DialogTitle> <DialogDescription> Click the card to flip. Use arrow keys to navigate. </DialogDescription> </DialogHeader> <div className="space-y-4 py-4"> <div className="flex items-center justify-between"> <div className="flex items-center gap-2"> <Badge variant="secondary"> {currentIndex + 1} / {activeCards.length} </Badge> {showMasteredOnly && ( <Badge variant="outline" className="text-primary"> Reviewing mastered </Badge> )} </div> <div className="flex items-center gap-2 text-sm text-muted-foreground"> <Sparkles className="h-4 w-4" /> <span>{masteredCount} mastered</span> </div> </div> <Progress value={progress} className="h-2" /> {allMastered && !showMasteredOnly ? ( <div className="flex flex-col items-center py-8 text-center"> <div className="rounded-full bg-primary/10 p-4 mb-4"> <Brain className="h-8 w-8 text-primary" /> </div> <h3 className="text-lg font-semibold">All cards mastered!</h3> <p className="text-sm text-muted-foreground mt-1"> Great job! You've learned all the cards. </p> <div className="flex gap-2 mt-4"> <Button variant="outline" onClick={handleReset}> <RotateCcw className="mr-2 h-4 w-4" /> Start Over </Button> <Button onClick={() => setShowMasteredOnly(true)}> Review All </Button> </div> </div> ) : activeCards.length === 0 ? ( <div className="flex flex-col items-center py-8 text-center"> <p className="text-sm text-muted-foreground"> No cards to review. </p> <Button variant="outline" className="mt-4" onClick={() => setShowMasteredOnly(false)} > Back to Learning </Button> </div> ) : ( <> <div className="relative h-48 cursor-pointer perspective-1000" onClick={handleFlip} role="button" tabIndex={0} aria-label={isFlipped ? "Show question" : "Show answer"} > <div className={cn( "absolute inset-0 transition-transform duration-500 transform-style-3d", isFlipped && "rotate-y-180" )} style={{ transformStyle: "preserve-3d", transform: isFlipped ? "rotateY(180deg)" : "rotateY(0deg)", }} > {/* Front */} <div className="absolute inset-0 rounded-lg border bg-card p-6 flex flex-col items-center justify-center text-center backface-hidden" style={{ backfaceVisibility: "hidden" }} > <Badge variant="outline" className="mb-3"> Question </Badge> <p className="text-lg font-medium">{currentCard?.front}</p> <p className="text-xs text-muted-foreground mt-4"> Click to reveal answer </p> </div> {/* Back */} <div className="absolute inset-0 rounded-lg border bg-primary/5 p-6 flex flex-col items-center justify-center text-center backface-hidden" style={{ backfaceVisibility: "hidden", transform: "rotateY(180deg)", }} > <Badge className="mb-3">Answer</Badge> <p className="text-lg">{currentCard?.back}</p> </div> </div> </div> <div className="flex items-center justify-between"> <Button variant="outline" size="icon" onClick={handlePrevious} disabled={activeCards.length <= 1} > <ChevronLeft className="h-4 w-4" /> </Button> <div className="flex gap-2"> {!showMasteredOnly && ( <> <Button variant="outline" size="sm" onClick={() => handleMarkMastered(false)} className="text-destructive hover:text-destructive" > <X className="mr-1 h-4 w-4" /> Still Learning </Button> <Button size="sm" onClick={() => handleMarkMastered(true)} > <Check className="mr-1 h-4 w-4" /> Mastered </Button> </> )} {showMasteredOnly && ( <Button variant="outline" size="sm" onClick={() => handleMarkMastered(false)} > <RotateCcw className="mr-1 h-4 w-4" /> Move to Learning </Button> )} </div> <Button variant="outline" size="icon" onClick={handleNext} disabled={activeCards.length <= 1} > <ChevronRight className="h-4 w-4" /> </Button> </div> <p className="text-xs text-center text-muted-foreground"> Press Space to flip • Arrow keys to navigate </p> </> )} </div> </DialogContent> </Dialog> );}Installation
npx shadcn@latest add https://www.shadcn.io/registry/dialog-flashcard.jsonnpx shadcn@latest add https://www.shadcn.io/registry/dialog-flashcard.jsonpnpm dlx shadcn@latest add https://www.shadcn.io/registry/dialog-flashcard.jsonbunx shadcn@latest add https://www.shadcn.io/registry/dialog-flashcard.jsonRelated blocks you will also like
React Dialog Block Habit Tracker
Learning habits
React Dialog Block Pomodoro Timer
Study sessions
React Dialog Block Quick Note
Study notes
React Dialog Block App Rating
Self-assessment
React Dialog Block Success Confirmation
Session complete
React Dialog Block Keyboard Shortcuts
Quick navigation