Prediction Gate
A lean predict-before-observation gate. The student picks one of several options; the parent flips answered and the gate reveals whatever you slot in children. Single-shot — once answered is true the option buttons lock — so the prediction is always on record by the time the reveal lands.
Distinct from BinaryPredictionGate (yes/no only), PredictionGateWithHints (multi-option with progressive hint ladder + correctness badges), and the picker-style PredictionGate under buttons/ (renders a Reveal button + correct/wrong feedback). Use this one when the parent owns the reveal trigger and the payload is whatever you slot in.
Before scrolling on — what does the next step do?
Installation
npx shadcn@latest add https://craftbits.dev/r/prediction-gate.jsonUsage
import { useState } from "react";
import { PredictionGate } from "@craft-bits/core/forms/prediction-gate";
const [answer, setAnswer] = useState<string | null>(null);
<PredictionGate
question="Before scrolling on — what does the next step do?"
options={[
{ value: "swap", label: "Swap the two elements" },
{ value: "skip", label: "Skip to the next pair" },
{ value: "stop", label: "Halt the loop" },
]}
answer={answer}
answered={answer !== null}
onAnswer={setAnswer}
>
The pointer advances — the swap branch isn't reached this iteration.
</PredictionGate>Defer the reveal to a downstream check by holding answered open until you are ready:
<PredictionGate
question="Will the running sum dip below zero?"
options={[
{ value: "yes", label: "Yes" },
{ value: "no", label: "No" },
]}
answer={answer}
answered={revealed}
onAnswer={setAnswer}
>
No — the next element is positive, so the sum strictly increases this step.
</PredictionGate>Use disabled to chain gates behind a prerequisite:
<PredictionGate
question="What does the closure capture?"
options={options}
answer={answer}
answered={answered}
onAnswer={setAnswer}
disabled={!unlocked}
/>Understanding the component
- Single-shot. Once
answeredistrue, the option buttons drop from focus and ignore taps. The prediction stays on record so the reveal feels like consequence rather than performance. - Parent owns the reveal. The gate never auto-flips
answered— the parent decides when to reveal. This keeps the gate composable with stepper UIs, scrub bars, and walk-through chrome. - Children as the payload. Anything you slot inside renders in a polite live region after the reveal — strings, paragraphs, code blocks, or whole sub-trees. The reveal block animates in with a smooth spring and is announced to screen readers.
- Controlled by
answer. PairanswerwithonAnswerto lift the pick into a parent reducer (useful for scoring, persistence, scrub-back replay). Omitanswerfor a self-contained gate that tracks its own pick. - Reduced motion. Tap feedback and reveal slide collapse to instant under
prefers-reduced-motion: reduce.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
question | ReactNode | required | Prompt rendered above the option buttons. |
options | PredictionGateOption[] | required | Ordered list of options (2-4 recommended). |
answered | boolean | required | When true, locks input and reveals children. |
answer | string | null | — | Controlled answer — the picked option's value. |
onAnswer | (value: string) => void | required | Fired once when the student picks. Receives the option value. |
children | ReactNode | — | Content revealed once answered is true. |
disabled | boolean | false | Force-disable without revealing. |
aria-label | string | "Prediction" | Accessible name for the gate region. |
className | string | — | Merged onto the outer <div>. |
PredictionGateOption
| Field | Type | Description |
|---|---|---|
value | string | Stable identifier — drives selection state and the onAnswer callback. |
label | ReactNode | Visible label for the option. |
Accessibility
- Root is a
role="group"labelled by the question paragraph. Inner buttons share arole="radiogroup"so screen readers announce them as a paired choice. - Each option is a
role="radio"witharia-checkedreflecting the student's pick. Disabled buttons drop from focus. - Every button has an
aria-labelderived from the visible label (or the optionvaluewhen a non-string label is passed). - The reveal block is wrapped in
role="status"andaria-live="polite"so the children content is announced without yanking focus. - Tap feedback collapses to instant under
prefers-reduced-motion: reduce. - Buttons clear the 44 x 44 px minimum touch target via
min-h-[44px]and horizontal padding.
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/interaction/PredictionGate.tsx). The source carried auseWidgetGate()phase-gate hookup, Fisher-Yates option shuffling, sound effects, per-distractor feedback maps, a hint slot, aHintLadderFeedbackpair, aFEEDBACK_MIN_DISPLAY_MSdebounce,LessonButton.PillContinue actions, and aRichTextrenderer keyed on lesson formatting. craft-bits collapses to the minimum prediction kernel — question, options, answered, onAnswer, children — and rewires every colour throughcb-*tokens so theme swaps repaint without prop changes.