Binary Prediction Gate

A two-button Yes/No gate placed in front of an explanation or animation. The student must pick a side before the parent flips revealed to disclose the truth. The gate is single-shot — once an answer lands it stops accepting input — so a prediction is always on record by the time the reveal plays.

Will the running sum ever go below zero on the next step?

Installation

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

Usage

import { useState } from "react";
import { BinaryPredictionGate } from "@craft-bits/core";
 
const [answer, setAnswer] = useState<boolean | null>(null);
const [revealed, setRevealed] = useState(false);
 
<BinaryPredictionGate
  question="Will the pointer cross the midpoint on the next step?"
  answer={answer}
  onAnswer={setAnswer}
  revealed={revealed}
  correctAnswer={true}
  explanation="Yes — the midpoint update advances by 4 cells, which lands past the midpoint."
/>

Uncontrolled — let the gate track its own pick:

<BinaryPredictionGate
  question="Will the sum stay positive on the next element?"
  onAnswer={(value) => console.log("picked", value)}
  revealed={revealed}
  correctAnswer={false}
/>

Override the button labels for True/False quizzes:

<BinaryPredictionGate
  question="Is the invariant preserved after this swap?"
  yesLabel="True"
  noLabel="False"
  onAnswer={setAnswer}
  revealed={revealed}
  correctAnswer={true}
/>

Understanding the component

  1. Single-shot. The gate locks after the first pick. Re-taps are ignored. This is the whole point — a prediction made under pressure stays on record, so the reveal feels like consequence rather than performance.
  2. Parent owns the reveal. The gate never auto-discloses. The parent flips revealed when it is ready to play the explanation. This keeps the gate composable with stepper UIs, scrub bars, and walk-through chrome.
  3. Quiz mode vs personal-prediction. Pass correctAnswer when the gate is a quiz — the buttons badge green / red on reveal. Omit it when the gate is a personal prediction (no right answer, just a commitment) — the chrome stays neutral.
  4. Controlled or uncontrolled. Pair answer with onAnswer to lift the pick into a parent reducer (useful for scoring, persistence, or scrub-back replay). Skip answer for a self-contained gate that tracks its own pick.
  5. Reveal explanation. Pass explanation to render a polite live-region block under the buttons after reveal. The block animates in with a smooth spring (or instantly under prefers-reduced-motion: reduce).

Props

PropTypeDefaultDescription
questionReactNoderequiredThe yes/no prompt rendered above the buttons.
onAnswer(answer: boolean) => voidrequiredFired once when the student picks. true = affirmative, false = negative.
revealedbooleanfalseWhen true, locks input and shows the reveal badges + explanation.
correctAnswerbooleanGround-truth answer. Drives green/red badges on reveal. Omit for personal-prediction mode.
answerboolean | nullControlled student answer. Pair with onAnswer.
yesLabelReactNode"Yes"Override the affirmative button label.
noLabelReactNode"No"Override the negative button label.
explanationReactNodeOptional explanation rendered after revealed flips true.
disabledbooleanfalseForce-disable without revealing.
aria-labelstring"Prediction"Accessible name for the question region.
classNamestringMerged onto the outer <div>.

Accessibility

  • The 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 "Yes" / "No" when a non-string label is passed).
  • The explanation block is wrapped in role="status" and aria-live="polite" so the reveal is announced without interrupting.
  • Tap feedback collapses to instant under prefers-reduced-motion: reduce (no scale animation, no reveal slide-in).
  • Buttons clear the 44 × 44 px minimum touch target via min-h-[44px] + horizontal padding.

Credits

  • Extracted from: algoflashcards (src/lessons/primitives/interaction/BinaryPredictionGate.tsx). The source exposed a PredictionGate.Binary / PredictionGate.Multi namespace coupled to lesson sound effects and Tailwind bg-success/10 literals. craft-bits collapses to a single binary gate, lifts the API to question + onAnswer + revealed, and rewires every colour through cb-* tokens so theme swaps repaint without prop changes.