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.jsonUsage
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
- 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.
- Parent owns the reveal. The gate never auto-discloses. The parent flips
revealedwhen it is ready to play the explanation. This keeps the gate composable with stepper UIs, scrub bars, and walk-through chrome. - Quiz mode vs personal-prediction. Pass
correctAnswerwhen 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. - Controlled or uncontrolled. Pair
answerwithonAnswerto lift the pick into a parent reducer (useful for scoring, persistence, or scrub-back replay). Skipanswerfor a self-contained gate that tracks its own pick. - Reveal explanation. Pass
explanationto render a polite live-region block under the buttons after reveal. The block animates in with a smooth spring (or instantly underprefers-reduced-motion: reduce).
Props
| Prop | Type | Default | Description |
|---|---|---|---|
question | ReactNode | required | The yes/no prompt rendered above the buttons. |
onAnswer | (answer: boolean) => void | required | Fired once when the student picks. true = affirmative, false = negative. |
revealed | boolean | false | When true, locks input and shows the reveal badges + explanation. |
correctAnswer | boolean | — | Ground-truth answer. Drives green/red badges on reveal. Omit for personal-prediction mode. |
answer | boolean | null | — | Controlled student answer. Pair with onAnswer. |
yesLabel | ReactNode | "Yes" | Override the affirmative button label. |
noLabel | ReactNode | "No" | Override the negative button label. |
explanation | ReactNode | — | Optional explanation rendered after revealed flips true. |
disabled | boolean | false | Force-disable without revealing. |
aria-label | string | "Prediction" | Accessible name for the question region. |
className | string | — | Merged onto the outer <div>. |
Accessibility
- The 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"Yes"/"No"when a non-string label is passed). - The explanation block is wrapped in
role="status"andaria-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 aPredictionGate.Binary/PredictionGate.Multinamespace coupled to lesson sound effects and Tailwindbg-success/10literals. craft-bits collapses to a single binary gate, lifts the API to question + onAnswer + revealed, and rewires every colour throughcb-*tokens so theme swaps repaint without prop changes.