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?

Customize
Reveal payload

Installation

npx shadcn@latest add https://craftbits.dev/r/prediction-gate.json

Usage

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

  1. Single-shot. Once answered is true, the option buttons drop from focus and ignore taps. The prediction stays on record so the reveal feels like consequence rather than performance.
  2. 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.
  3. 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.
  4. Controlled by answer. Pair answer with onAnswer to lift the pick into a parent reducer (useful for scoring, persistence, scrub-back replay). Omit answer for a self-contained gate that tracks its own pick.
  5. Reduced motion. Tap feedback and reveal slide collapse to instant under prefers-reduced-motion: reduce.

Props

PropTypeDefaultDescription
questionReactNoderequiredPrompt rendered above the option buttons.
optionsPredictionGateOption[]requiredOrdered list of options (2-4 recommended).
answeredbooleanrequiredWhen true, locks input and reveals children.
answerstring | nullControlled answer — the picked option's value.
onAnswer(value: string) => voidrequiredFired once when the student picks. Receives the option value.
childrenReactNodeContent revealed once answered is true.
disabledbooleanfalseForce-disable without revealing.
aria-labelstring"Prediction"Accessible name for the gate region.
classNamestringMerged onto the outer <div>.

PredictionGateOption

FieldTypeDescription
valuestringStable identifier — drives selection state and the onAnswer callback.
labelReactNodeVisible label for the option.

Accessibility

  • Root is a role="group" labelled by the question paragraph. Inner buttons share a role="radiogroup" so screen readers announce them as a paired choice.
  • Each option is a role="radio" with aria-checked reflecting the student's pick. Disabled buttons drop from focus.
  • Every button has an aria-label derived from the visible label (or the option value when a non-string label is passed).
  • The reveal block is wrapped in role="status" and aria-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 a useWidgetGate() phase-gate hookup, Fisher-Yates option shuffling, sound effects, per-distractor feedback maps, a hint slot, a HintLadderFeedback pair, a FEEDBACK_MIN_DISPLAY_MS debounce, LessonButton.Pill Continue actions, and a RichText renderer keyed on lesson formatting. craft-bits collapses to the minimum prediction kernel — question, options, answered, onAnswer, children — and rewires every colour through cb-* tokens so theme swaps repaint without prop changes.