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

Usage

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

  1. 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 shared reveal paragraph rides underneath as the canonical rule.
  2. 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.
  3. Controlled or uncontrolled. Pair value with onAnswer to lift the pick into a parent reducer (useful for telemetry, persistence, or scrub-back replay). Skip value for a self-contained quiz that tracks its own pick via defaultValue.
  4. Reveal animation. The reveal block uses a smooth spring on entrance and exit. Under prefers-reduced-motion: reduce the animation collapses to a simple opacity fade.
  5. Lesson-system free. The source had a slug / lessonId couple plus a track analytics call and a useWidgetHistory undo-stack. craft-bits strips all of that — wire onAnswer to your own tracker, parent reducer, or history hook if you want telemetry.

Props

PropTypeDefaultDescription
questionReactNoderequiredThe prompt rendered above the options.
optionsreadonly PredictMcqOption[]requiredOrdered list of options the student can tap.
correctstringrequiredThe value of the correct option.
valuestring | nullControlled pick. Pair with onAnswer.
defaultValuestring | nullnullInitial pick for the uncontrolled mode.
onAnswer(event) => voidFires on every pick. Event is { value, correct }.
revealReactNodeShared teaching paragraph rendered under the per-option explanation.
eyebrowReactNode"Predict"Eyebrow label above the question.
correctHeadingReactNode"Got it"Reveal block heading on a correct pick.
wrongHeadingReactNode"Not this one — here's why"Reveal block heading on a wrong pick.
disabledbooleanfalseForce-disable the quiz without revealing.
aria-labelstring"Prediction"Accessible name for the option group.
classNamestringMerged onto the outer <div>.

PredictMcqOption

FieldTypeDefaultDescription
valuestringrequiredStable identifier — compared against correct.
labelReactNoderequiredVisible label rendered inside the option button.
explanationReactNodePer-option diagnosis rendered inside the reveal block on pick.

Accessibility

  • The root is a role="group" labelled by the question paragraph. Options share a role="radiogroup" so screen readers announce them as a choice set; the picked option's aria-checked flips to true.
  • Each option button has an aria-label derived from the visible label (or the option value when a non-string label is passed). Buttons clear the 44 x 44 px minimum hit area via min-h-[44px] + horizontal padding.
  • The reveal block is wrapped in role="status" and aria-live="polite" so the diagnosis is announced when the learner picks; the option group references it via aria-describedby once a pick lands.
  • Focus is visible across every option via a token-driven focus-visible ring (--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 analytics track event, and the Widget chrome wrapper. craft-bits strips lesson chrome, generalises the option shape to { value, label, explanation? }, threads the colours through cb-* tokens, and routes motion through the shared SPRINGS.* set so the primitive ships free of the source project.