Predict MCQ
A multiple-choice prediction checkpoint placed in front of an explanation. The learner picks one of the options; the chosen pill lights up correct or wrong, and a reveal block slides open below the list carrying the option's own diagnosis plus an optional shared teaching paragraph. The quiz is not single-shot — tapping another option swaps the highlight and the explanation, so the learner can read every wrong option's diagnosis before settling on the right answer.
Predict
Which data structure makes Two-Sum run in linear time?
Installation
npx shadcn@latest add https://craftbits.dev/r/predict-mcq.jsonUsage
import { useState } from "react";
import { PredictMcq, type PredictMcqOption } from "@craft-bits/core";
const options: PredictMcqOption[] = [
{
value: "linear",
label: "Search through the array one element at a time",
explanation: "Linear scan is O(n) per query — hash maps beat it.",
},
{
value: "hash",
label: "Store each value in a Map keyed by the value itself",
explanation: "Right — O(n) to build, O(1) lookups afterwards.",
},
];
const [picked, setPicked] = useState<string | null>(null);
<PredictMcq
question="Which data structure makes Two-Sum run in linear time?"
options={options}
correct="hash"
value={picked}
onAnswer={(e) => setPicked(e.value)}
reveal="The rule: when a problem asks 'is X in this set?', a hash structure turns it into an O(1) lookup."
/>Uncontrolled — let the quiz track its own pick:
<PredictMcq
question="Pick the tightest big-O for inserting into a balanced BST."
options={[
{ value: "log", label: "O(log n)" },
{ value: "lin", label: "O(n)", explanation: "Only the worst case of an unbalanced tree." },
]}
correct="log"
onAnswer={(e) => console.log(e)}
/>Custom heading and eyebrow copy for a quiz with a different tone:
<PredictMcq
question="What's the invariant after the merge step?"
options={mergeOptions}
correct="sorted-prefix"
eyebrow="Quick check"
correctHeading="Locked in"
wrongHeading="Re-read the invariant"
onAnswer={handle}
/>Understanding the component
- Per-option diagnosis. Every option can carry its own
explanation. When the learner picks an option, the reveal block animates open with that explanation — wrong options get a "here's why you'd think that" line, the right option gets a confirmation. The sharedrevealparagraph rides underneath as the canonical rule. - Re-pickable on purpose. The quiz is not single-shot. Subsequent taps swap the highlight and the explanation, so the learner can chase every wrong answer's diagnosis before settling on the correct one. The "tip" line under the reveal nudges this behaviour after a wrong pick.
- Controlled or uncontrolled. Pair
valuewithonAnswerto lift the pick into a parent reducer (useful for telemetry, persistence, or scrub-back replay). Skipvaluefor a self-contained quiz that tracks its own pick viadefaultValue. - Reveal animation. The reveal block uses a smooth spring on entrance and exit. Under
prefers-reduced-motion: reducethe animation collapses to a simple opacity fade. - Lesson-system free. The source had a
slug/lessonIdcouple plus atrackanalytics call and auseWidgetHistoryundo-stack. craft-bits strips all of that — wireonAnswerto your own tracker, parent reducer, or history hook if you want telemetry.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
question | ReactNode | required | The prompt rendered above the options. |
options | readonly PredictMcqOption[] | required | Ordered list of options the student can tap. |
correct | string | required | The value of the correct option. |
value | string | null | — | Controlled pick. Pair with onAnswer. |
defaultValue | string | null | null | Initial pick for the uncontrolled mode. |
onAnswer | (event) => void | — | Fires on every pick. Event is { value, correct }. |
reveal | ReactNode | — | Shared teaching paragraph rendered under the per-option explanation. |
eyebrow | ReactNode | "Predict" | Eyebrow label above the question. |
correctHeading | ReactNode | "Got it" | Reveal block heading on a correct pick. |
wrongHeading | ReactNode | "Not this one — here's why" | Reveal block heading on a wrong pick. |
disabled | boolean | false | Force-disable the quiz without revealing. |
aria-label | string | "Prediction" | Accessible name for the option group. |
className | string | — | Merged onto the outer <div>. |
PredictMcqOption
| Field | Type | Default | Description |
|---|---|---|---|
value | string | required | Stable identifier — compared against correct. |
label | ReactNode | required | Visible label rendered inside the option button. |
explanation | ReactNode | — | Per-option diagnosis rendered inside the reveal block on pick. |
Accessibility
- The root is a
role="group"labelled by the question paragraph. Options share arole="radiogroup"so screen readers announce them as a choice set; the picked option'saria-checkedflips totrue. - Each option button has an
aria-labelderived from the visible label (or the optionvaluewhen a non-string label is passed). Buttons clear the 44 x 44 px minimum hit area viamin-h-[44px]+ horizontal padding. - The reveal block is wrapped in
role="status"andaria-live="polite"so the diagnosis is announced when the learner picks; the option group references it viaaria-describedbyonce a pick lands. - Focus is visible across every option via a token-driven
focus-visiblering (--cb-accent). - Tap feedback and the reveal slide collapse to instant under
prefers-reduced-motion: reduce.
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/interaction/PredictMcq.tsx). The original was wired into the lesson runtime —slug,useWidgetHistory(undo/redo stack), the analyticstrackevent, and theWidgetchrome wrapper. craft-bits strips lesson chrome, generalises the option shape to{ value, label, explanation? }, threads the colours throughcb-*tokens, and routes motion through the sharedSPRINGS.*set so the primitive ships free of the source project.