Code Predict
A read-then-predict primitive. The card renders a code snippet and a row of answer chips; the learner mentally executes the snippet, picks the value it should evaluate to, and only then continues reading. Wrong picks shake and auto-clear so the row stays tappable; the first correct pick locks the row with a green pop and reveals optional follow-up content.
Run it in your head
What does the snippet log?
const xs = [1, 2, 3];
const ys = xs.map((x) => x * 2);
console.log(ys[2]);Pick the value the snippet evaluates to.
Installation
npx shadcn@latest add https://craftbits.dev/r/code-predict.jsonUsage
import { CodePredict } from "@craft-bits/core";
<CodePredict
code={snippet}
prompt="What does the snippet log?"
options={["3", "6", "2", "undefined"]}
correct="6"
onAnswer={(e) => console.log(e.value, e.correct)}
reveal="xs.map((x) => x * 2) produces [2, 4, 6], so index 2 is 6."
/>Use { value, label } option objects when the visible chip should differ from the underlying value (e.g. JSX labels):
<CodePredict
code={snippet}
options={[
{ value: "a", label: <code>[1]</code> },
{ value: "b", label: <code>[0]</code> },
{ value: "c", label: <code>[]</code> },
]}
correct="a"
onAnswer={handleAnswer}
/>Swap the options array to re-use the component for the next question — the internal pick state resets automatically when the array identity changes.
Understanding the component
- Read, predict, reveal. The card stacks an optional eyebrow + prompt, a monospaced code block, and a chip row. The prompt is the question, the chips are the answers, and the optional
revealis the follow-up that only opens once the learner lands the correct pick — keeping reading and answering on a single rhythm. - Single-shot once correct. The first correct pick locks the row with a brief scale pop and surfaces
reveal. Subsequent taps are ignored. Wrong picks shake (~600 ms) and clear so the row stays tappable — no parent state required. - String or object options. Bare strings collapse the
valueandlabelinto the same string. Use{ value, label }when the visible chip should differ — JSX labels, codeblock labels, or short tokens with long display text. - Re-using the row. The internal pick state resets whenever the
optionsarray identity changes. Swap a new array (or memoize per question) to ask the next question without remounting. - Reduced motion. Under
prefers-reduced-motion: reducethe shake, scale pop, and reveal slide all collapse to instant — the row still flips colour but skips the animation.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
code | string | required | Code snippet shown above the answer row. |
eyebrow | ReactNode | 'Run it in your head' | Small label rendered above the prompt. |
prompt | ReactNode | — | Optional question rendered between the eyebrow and the code block. |
options | readonly (string | { value: string; label?: ReactNode })[] | required | Choices the learner picks from. |
correct | string | required | Value of the correct option. |
onAnswer | (event: { value: string; correct: boolean }) => void | — | Fires on every pick — once with correct: true, plus one per wrong attempt. |
reveal | ReactNode | — | Follow-up content opened under the row on the winning pick. |
successCaption | ReactNode | 'Correct.' | Caption shown once the learner lands the answer. |
failCaption | ReactNode | 'Not quite — try again.' | Caption shown after a wrong pick. |
idleCaption | ReactNode | 'Pick the value the snippet evaluates to.' | Caption shown before any pick. |
language | string | — | Syntax language hint surfaced as data-language on the code block. |
disabled | boolean | false | Force-disable picks without resolving. |
aria-label | string | 'Predict the output' | Accessible name for the row. |
className | string | — | Merged onto the outer container. |
Accessibility
- The root is a
role="group"labelled by either the prompt (when provided) oraria-label. The chip row is arole="radiogroup"and each chip is arole="radio"whosearia-checkedreflects the winning option. - Each chip carries an
aria-labelderived from a string label, falling back to the optionvaluewhen the label is JSX so screen readers always announce something meaningful. - The caption lives inside an
aria-live="polite"region so result changes are announced without stealing focus. - Chips clear the 40 px minimum hit target recommended by WCAG 2.5.8 — keyboard
Enter/Spaceactivate the radio just like a native control. - Animations collapse to instant under
prefers-reduced-motion: reduce: no shake, no scale pop, no reveal slide. - Focus is visible across every chip via a token-driven
focus-visiblering (--cb-accent).
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/interaction/CodePredict.tsx). The source was a free-text predictor wired into the lesson runtime — it depended onWidget,useWidgetHistory, a slug-keyed analyticstrackcall, and a markdownrenderInlinehelper, and surfaced a per-componentcodePredictDiffheuristic for numeric distance feedback. craft-bits generalises it to a stand-alone multiple-choice predictor: lesson chrome dropped, the free-text input replaced with a chip row so the component owns its answer vocabulary, theaccept/reveal/commonMistakesprops collapsed intooptions+correct+ an optionalrevealnode, and every colour rewired throughcb-accent/cb-success/cb-errortokens so theme swaps repaint without prop changes.