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.json

Usage

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

  1. 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.
  2. 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.
  3. Two callbacks, two intents. onAnswer fires on every tap (correct or wrong) for analytics. onPass fires once, only on the first correct pick — wire it to start a lesson timer or unlock a checkpoint.
  4. 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.
  5. Theme-driven colours. Buttons paint through cb-accent, cb-success, and cb-error tokens — swap the theme and every state repaints with no prop changes.

Props

PropTypeDefaultDescription
questionReactNoderequiredWarm-up question rendered above the options.
optionsreadonly { id: string; label: ReactNode }[]requiredOrdered list of options (2-4 recommended).
correctIdstringrequiredid of the correct option.
childrenReactNoderequiredContent revealed once the student passes the gate.
onAnswer(event) => voidFires on every tap with { id, correct }.
onPass() => voidFires once on the first correct pick.
disabledbooleanfalseForce-disable taps without resolving.
aria-labelstring"Warm-up gate"Accessible name for the gate region.
classNamestringMerged onto the outer container.

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 set on the winning option. Disabled buttons drop from focus.
  • Every option carries an aria-label derived from a string label, falling back to the option id for 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 separate PredictionGate primitive plus a hardcoded trackHex colour prop and a six-field distractor-feedback contract. craft-bits collapses to a single self-contained gate with a flat options + correctId API, repaints every state through cb-accent / cb-success / cb-error tokens, and lifts the resolve hook into onPass so callers can wire a checkpoint without subscribing to every wrong tap.