Quiz
Interactive multiple-choice quiz with hints, explanations, and scoring. Install Quiz from the VLLNT UI registry with the shadcn CLI.
What it is and when to use
Quiz is an interactive multiple-choice component for a single question. Learners pick one option and press Check Answer; the component marks the choice correct or incorrect, reveals the right option, and can show a per-option explanation plus an overall result panel. An optional hint toggle and a Try Again reset are built in, and an onAnswer callback reports whether the answer was right. Use it to embed knowledge checks in tutorials, docs, or courseware.
LearningInteractive multiple-choice quiz with hints, explanations, and scoring. Part of the learning family in VLLNT UI, it ships as a machine-readable registry entry — copy the source directly into your app with the shadcn CLI and own it, no runtime dependency on a component library.
Preview
Switch between light and dark to inspect the embedded Storybook preview.
Installation
Add Quiz to your project with the shadcn CLI. The source lands in your codebase, ready to adapt:
pnpm dlx shadcn@latest add https://ui.vllnt.com/r/quiz.jsonSource
"use client";
import { type ReactNode, useState } from "react";
import type { HeadingTag } from "../../lib/types";
import { cn } from "../../lib/utils";
export type QuizOption = {
correct?: boolean;
explanation?: string;
label: string;
};
type QuizOptionButtonProps = {
index: number;
onSelect: (index: number) => void;
option: QuizOption;
selectedIndex: null | number;
submitted: boolean;
};
// eslint-disable-next-line max-lines-per-function -- Option button showing selected, correct, incorrect states
function QuizOptionButton({
index,
onSelect,
option,
selectedIndex,
submitted,
}: QuizOptionButtonProps): React.ReactNode {
const isSelected = selectedIndex === index;
const showResult = submitted && isSelected;
return (
<button
className={cn(
"w-full text-left p-3 rounded-md border transition-colors hover:bg-muted/50 focus:outline-none focus:ring-2 focus:ring-primary",
isSelected && !submitted && "border-primary bg-primary/10",
showResult && option.correct && "border-green-500 bg-green-500/10",
showResult && !option.correct && "border-red-500 bg-red-500/10",
submitted &&
!isSelected &&
option.correct &&
"border-green-500/50 bg-green-500/5",
)}
disabled={submitted}
onClick={() => {
onSelect(index);
}}
type="button"
>
<div className="flex items-center justify-between">
<span className="text-sm">{option.label}</span>
{showResult ? (
option.correct ? (
<svg
className="size-4 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M5 13l4 4L19 7"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
/>
</svg>
) : (
<svg
className="size-4 text-red-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M6 18L18 6M6 6l12 12"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
/>
</svg>
)
) : null}
{submitted && !isSelected && option.correct ? (
<svg
className="size-4 text-green-500/50"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M5 13l4 4L19 7"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
/>
</svg>
) : null}
</div>
{submitted && option.explanation ? (
<p className="text-xs text-muted-foreground mt-2">
{option.explanation}
</p>
) : null}
</button>
);
}
type QuizHintProps = {
hint: string;
onShow: () => void;
showHint: boolean;
};
function QuizHint({ hint, onShow, showHint }: QuizHintProps): React.ReactNode {
return (
<div className="mb-4">
{showHint ? (
<p className="text-sm text-muted-foreground italic bg-muted/50 p-2 rounded">
{hint}
</p>
) : (
<button
className="text-sm text-primary hover:underline"
onClick={onShow}
type="button"
>
Show hint
</button>
)}
</div>
);
}
type QuizResultProps = {
explanation: ReactNode;
isCorrect: boolean;
};
function QuizResult({
explanation,
isCorrect,
}: QuizResultProps): React.ReactNode {
return (
<div
className={cn(
"p-3 rounded-md text-sm mb-4",
isCorrect
? "bg-green-500/10 text-green-700 dark:text-green-300"
: "bg-muted",
)}
>
<p className="font-medium mb-1">
{isCorrect ? "Correct!" : "Not quite right."}
</p>
<div className="[&>p]:mb-0">{explanation}</div>
</div>
);
}
export type QuizProps = {
/** Heading tag for the question. Defaults to `h4`. */
as?: HeadingTag;
className?: string;
explanation?: ReactNode;
hint?: string;
onAnswer?: (correct: boolean) => void;
options: QuizOption[];
question: string;
};
// eslint-disable-next-line max-lines-per-function -- Interactive quiz with state management
export function Quiz({
as: Heading = "h4",
className,
explanation,
hint,
onAnswer,
options,
question,
}: QuizProps): React.ReactNode {
const [selectedIndex, setSelectedIndex] = useState<null | number>(null);
const [showHint, setShowHint] = useState(false);
const [submitted, setSubmitted] = useState(false);
const handleReset = (): void => {
setSelectedIndex(null);
setSubmitted(false);
setShowHint(false);
};
const handleSubmit = (): void => {
setSubmitted(true);
const isCorrect = selectedIndex !== null && options[selectedIndex]?.correct;
onAnswer?.(Boolean(isCorrect));
};
const isCorrect = selectedIndex !== null && options[selectedIndex]?.correct;
return (
<div className={cn("my-6 rounded-lg border bg-card p-6", className)}>
<div className="flex items-start gap-3 mb-4">
<svg
className="size-5 text-primary flex-shrink-0 mt-0.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
/>
</svg>
<Heading className="font-semibold text-foreground">{question}</Heading>
</div>
<div className="space-y-2 mb-4">
{options.map((opt, index) => (
<QuizOptionButton
index={index}
key={`quiz-option-${index}-${opt.label}`}
onSelect={(index_) => {
if (!submitted) setSelectedIndex(index_);
}}
option={opt}
selectedIndex={selectedIndex}
submitted={submitted}
/>
))}
</div>
{hint && !submitted ? (
<QuizHint
hint={hint}
onShow={() => {
setShowHint(true);
}}
showHint={showHint}
/>
) : null}
{submitted && explanation ? (
<QuizResult explanation={explanation} isCorrect={Boolean(isCorrect)} />
) : null}
<div className="flex gap-2">
{submitted ? (
<button
className="px-3 py-1.5 text-sm rounded-md border border-border hover:bg-muted transition-colors"
onClick={handleReset}
type="button"
>
Try Again
</button>
) : (
<button
className="px-3 py-1.5 text-sm rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:pointer-events-none transition-colors"
disabled={selectedIndex === null}
onClick={handleSubmit}
type="button"
>
Check Answer
</button>
)}
</div>
</div>
);
}
Stories
Explore every variant and state in the interactive Storybook:
Preview
Switch between light and dark to inspect the embedded Storybook preview.
FAQ
- What is Quiz?
- Quiz is a React multiple-choice component from VLLNT UI that presents one question with several options. It validates the selected answer, highlights correct and incorrect choices, and can display hints and explanations for self-paced learning.
- How do I add Quiz?
- Run `pnpm dlx shadcn@latest add https://ui.vllnt.com/r/quiz.json` to install Quiz via the shadcn CLI. The component and its types are copied into your project so you can customize them.
- Does Quiz show hints and explanations?
- Yes. Pass a hint string to reveal a Show hint toggle, give any option an explanation shown after submitting, and pass an explanation node for the result panel. An onAnswer callback fires with whether the choice was correct.