Warmup Gate
A single prediction question that gates a block of content. The student answers the warm-up first; on a correct pick the gate fades out and the wrapped children fade in. Wrong picks shake red and stay tappable. Use it before a lesson, a code walkthrough, or any content where a one-question primer focuses attention.
Before we start: which traversal visits the root first?
- Pick an option to log an event.
Installation
npx shadcn@latest add https://craftbits.dev/r/warmup-gate.jsonUsage
import { WarmupGate } from "@craft-bits/core";
<WarmupGate
question="Before we start: which traversal visits the root first?"
options={[
{ id: "pre", label: "Pre-order" },
{ id: "in", label: "In-order" },
{ id: "post", label: "Post-order" },
{ id: "lvl", label: "Level-order" },
]}
correctId="pre"
onAnswer={(e) => console.log(e.id, e.correct)}
onPass={() => console.log("unlocked")}
>
<LessonBody />
</WarmupGate>Children mount only after the gate passes, so heavy content stays cheap until the student earns it:
<WarmupGate
question="Pick the tighter bound."
options={[
{ id: "a", label: "O(n)" },
{ id: "b", label: "O(n log n)" },
]}
correctId="a"
onPass={startLessonTimer}
>
<ExpensiveVisualization />
</WarmupGate>Understanding the component
- Single-shot once correct. The gate ignores all input after the first correct pick. Wrong picks badge red, shake, and stay tappable so the student can keep trying — no parent state required.
- Children mount on pass. Wrapped content is unmounted until the gate resolves. Expensive visualizations, video, and live code panels stay cheap until the student earns them.
- Two callbacks, two intents.
onAnswerfires on every tap (correct or wrong) for analytics.onPassfires once, only on the first correct pick — wire it to start a lesson timer or unlock a checkpoint. - Crossfade on resolve. The gate exits with a downward fade while the children enter with an upward fade, both on
SPRINGS.smooth. Reduced-motion users get an instant swap. - Theme-driven colours. Buttons paint through
cb-accent,cb-success, andcb-errortokens — swap the theme and every state repaints with no prop changes.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
question | ReactNode | required | Warm-up question rendered above the options. |
options | readonly { id: string; label: ReactNode }[] | required | Ordered list of options (2-4 recommended). |
correctId | string | required | id of the correct option. |
children | ReactNode | required | Content revealed once the student passes the gate. |
onAnswer | (event) => void | — | Fires on every tap with { id, correct }. |
onPass | () => void | — | Fires once on the first correct pick. |
disabled | boolean | false | Force-disable taps without resolving. |
aria-label | string | "Warm-up gate" | Accessible name for the gate region. |
className | string | — | Merged onto the outer container. |
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-checkedset on the winning option. Disabled buttons drop from focus. - Every option carries an
aria-labelderived from a string label, falling back to the optionidfor non-string labels. - The outer container is an
aria-live="polite"region so the transition from question to content is announced without interrupting. - Tap feedback collapses to instant under
prefers-reduced-motion: reduce— no shake, no scale pop, no fade swap. - Option buttons clear the 44 x 44 px minimum touch target via
min-h-[44px]and horizontal padding.
Credits
- Extracted from:
algoflashcards(src/lessons/primitives/interaction/WarmupGate.tsx). The source was a thin wrapper around a separatePredictionGateprimitive plus a hardcodedtrackHexcolour prop and a six-field distractor-feedback contract. craft-bits collapses to a single self-contained gate with a flatoptions+correctIdAPI, repaints every state throughcb-accent/cb-success/cb-errortokens, and lifts the resolve hook intoonPassso callers can wire a checkpoint without subscribing to every wrong tap.