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

Usage

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

  1. 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 reveal is the follow-up that only opens once the learner lands the correct pick — keeping reading and answering on a single rhythm.
  2. 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.
  3. String or object options. Bare strings collapse the value and label into the same string. Use { value, label } when the visible chip should differ — JSX labels, codeblock labels, or short tokens with long display text.
  4. Re-using the row. The internal pick state resets whenever the options array identity changes. Swap a new array (or memoize per question) to ask the next question without remounting.
  5. Reduced motion. Under prefers-reduced-motion: reduce the shake, scale pop, and reveal slide all collapse to instant — the row still flips colour but skips the animation.

Props

PropTypeDefaultDescription
codestringrequiredCode snippet shown above the answer row.
eyebrowReactNode'Run it in your head'Small label rendered above the prompt.
promptReactNodeOptional question rendered between the eyebrow and the code block.
optionsreadonly (string | { value: string; label?: ReactNode })[]requiredChoices the learner picks from.
correctstringrequiredValue of the correct option.
onAnswer(event: { value: string; correct: boolean }) => voidFires on every pick — once with correct: true, plus one per wrong attempt.
revealReactNodeFollow-up content opened under the row on the winning pick.
successCaptionReactNode'Correct.'Caption shown once the learner lands the answer.
failCaptionReactNode'Not quite — try again.'Caption shown after a wrong pick.
idleCaptionReactNode'Pick the value the snippet evaluates to.'Caption shown before any pick.
languagestringSyntax language hint surfaced as data-language on the code block.
disabledbooleanfalseForce-disable picks without resolving.
aria-labelstring'Predict the output'Accessible name for the row.
classNamestringMerged onto the outer container.

Accessibility

  • The root is a role="group" labelled by either the prompt (when provided) or aria-label. The chip row is a role="radiogroup" and each chip is a role="radio" whose aria-checked reflects the winning option.
  • Each chip carries an aria-label derived from a string label, falling back to the option value when 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/Space activate 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-visible ring (--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 on Widget, useWidgetHistory, a slug-keyed analytics track call, and a markdown renderInline helper, and surfaced a per-component codePredictDiff heuristic 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, the accept / reveal / commonMistakes props collapsed into options + correct + an optional reveal node, and every colour rewired through cb-accent / cb-success / cb-error tokens so theme swaps repaint without prop changes.