Code Fill V2

A children-mode fill-in-the-blank code editor. Plain code fragments interleave with <BlankSlotV2> placeholders inline — no Shiki template string, no marker syntax. The surrounding <CodeFillV2> owns answer state, runs a single submit pass over every blank, and surfaces per-option feedback below the failure banner on wrong picks. Companion to the template-string variant at /docs/forms/code-fill.

function twoSum(nums: number[], target: number): number[] {
const seen = ;
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if () {
return [seen.get(complement)!, i];
}
seen.set(nums[i], i);
}
return [];
}

Installation

npx shadcn@latest add https://craftbits.dev/r/code-fill-v2.json

No Shiki dependency — the template is rendered as plain monospace text inside the code surface.

Usage

import {
  BlankSlotV2,
  CodeFillV2,
  type CodeFillV2Blank,
} from "@craft-bits/core";
 
const blanks: CodeFillV2Blank[] = [
  {
    id: "ctor",
    label: "data structure",
    options: ["new Map()", "new Set()", "[]"],
    correctIdx: 0,
  },
  {
    id: "lookup",
    label: "lookup",
    options: ["seen.has(complement)", "seen.get(complement)"],
    correctIdx: 0,
  },
];
 
<CodeFillV2 blanks={blanks} onComplete={() => console.log("solved")}>
  <div>function twoSum(nums, target) &#123;</div>
  <div>
    const seen = <BlankSlotV2 blank={blanks[0]} />;
  </div>
  <div>
    if (<BlankSlotV2 blank={blanks[1]} />) return [seen.get(complement), i];
  </div>
  <div>&#125;</div>
</CodeFillV2>

Surface per-option feedback so wrong picks land on a specific misconception:

const blanks: CodeFillV2Blank[] = [
  {
    id: "ctor",
    label: "data structure",
    options: ["new Map()", "new Set()", "[]"],
    correctIdx: 0,
    optionFeedback: {
      1: "A Set only stores keys — we need key to index lookup.",
      2: "Arrays do not give O(1) lookup by value.",
    },
    wrongFeedback: "We need key to index lookup.",
  },
];

Replace the success state with a custom reveal — a separately highlighted finished snippet, a celebration animation, or downstream content the parent owns:

<CodeFillV2 blanks={blanks} successContent={<FinishedCodeBlock code={completed} />}>
  {template}
</CodeFillV2>

Understanding the component

  1. Children, not a template string. Authors compose the code surface as inline JSX — every blank lives in the spot it visually belongs. There is no marker syntax to learn, no template parser, and no constraint on what counts as a "line" — wrap the template however you want.
  2. One submit, every blank checked. Picking an option in any dropdown is a live update; correctness is evaluated only when the learner taps the submit button. Every wrong blank then surfaces its optionFeedback entry (or falls back to wrongFeedback) so each misconception lands on a specific note.
  3. Live correct callback, fire once. onBlankCorrect is the moment-of-discovery hook for accreting side panels. It fires at most once per blank — switching to a wrong option and back does not re-fire — which keeps progressive reveal layouts well behaved.
  4. Self-resetting. After a wrong submit the learner can retry: every answer clears, every active dropdown closes, and the success/failure state resets. The retry button mirrors the submit button so the muscle memory is the same on both sides of the gate.
  5. Bring your own reveal. Pass successContent to fully replace the template on a correct submit. Omit it to keep the now-resolved template on screen.

Props

PropTypeDefaultDescription
blanksreadonly CodeFillV2Blank[]requiredBlank descriptors. Each id must match a <BlankSlotV2 blank={...}> child.
childrenReactNoderequiredJSX template — plain code text interleaved with <BlankSlotV2> placeholders.
successContentReactNodeReplaces the template on a fully-correct submit.
onComplete() => voidFires on a fully correct submit.
onWrong() => voidFires on a submit with one or more wrong blanks.
onBlankWrong(blankId, pickedIdx) => voidFires once per wrong blank on a wrong submit.
onBlankCorrect(blankId) => voidFires the first time the learner picks the correct option for a blank.
successMessageReactNode'All correct!'Banner shown on success.
successExtraReactNodeExtra content below the success banner.
failMessageReactNode'Not quite — check the red blanks.'Banner shown on failure.
submitLabelReactNode'Check'Submit button label.
retryLabelReactNode'Try again'Retry button label.
codeClassNamestringExtra className on the code wrapper.
classNamestringExtra className on the outer wrapper.

BlankSlotV2

PropTypeDefaultDescription
blankCodeFillV2BlankrequiredThe blank descriptor this slot represents.
minWidthnumber120Minimum width of the blank button (px).
dropdownMinWidthnumberminWidth + 60Minimum width of the options dropdown (px).

CodeFillV2Blank

FieldTypeDefaultDescription
idstringrequiredStable identifier — must be unique within the blanks array.
labelstringrequiredShort label shown when the blank is empty.
optionsreadonly string[]requiredChoice strings in display order.
correctIdxnumberrequiredIndex of the correct option in options.
wrongFeedbackReactNodeFallback feedback shown after a wrong submit on this blank.
optionFeedbackPartial<Record<number, ReactNode>>Feedback keyed by option index — wins over wrongFeedback when present.

Accessibility

  • Each blank is a real <button> with aria-haspopup="listbox", aria-expanded, and an aria-label that announces the slot's label and current value (or "not filled").
  • The dropdown is a role="listbox" whose options use role="option" and reflect selection through aria-selected. Both blank buttons and options enforce a 44px minimum hit area (WCAG 2.5.8 AAA).
  • Submit and retry banners live inside an aria-live="polite" region so screen readers announce the result when it appears.
  • Focus is visible across blanks, options, and CTA buttons via a token-driven focus-visible ring (--cb-accent).
  • The wrong-blank shake animation is a single, brief horizontal shimmy with a damped spring — no flashing or sustained motion.
  • Per-option feedback is rendered as text inside the live region so screen readers receive the explanation, not just the buzz.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/interaction/CodeFill.tsx). The original was wired into the lesson runtime — useWidgetGate from the Lesson shell, playSound, LessonButton.Pill, and the project-specific CodeTrace success view. craft-bits generalises it to a stand-alone primitive: lesson chrome dropped, the submit and retry buttons replaced with the canonical accent-pill recipe so the primitive ships without LessonButton as a dependency, every colour routed through cb-* tokens, every transition routed through SPRINGS.snap / SPRINGS.smooth, and the success-state reveal made optional via successContent (the original always swapped to a CodeTrace).
  • Companion to the Shiki-template-string variant at /docs/forms/code-fill (extracted from AlgoFlashcards) — same submit model, different authoring surface.