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.jsonNo 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) {</div>
<div>
const seen = <BlankSlotV2 blank={blanks[0]} />;
</div>
<div>
if (<BlankSlotV2 blank={blanks[1]} />) return [seen.get(complement), i];
</div>
<div>}</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
- 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.
- 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
optionFeedbackentry (or falls back towrongFeedback) so each misconception lands on a specific note. - Live correct callback, fire once.
onBlankCorrectis 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. - 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.
- Bring your own reveal. Pass
successContentto fully replace the template on a correct submit. Omit it to keep the now-resolved template on screen.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
blanks | readonly CodeFillV2Blank[] | required | Blank descriptors. Each id must match a <BlankSlotV2 blank={...}> child. |
children | ReactNode | required | JSX template — plain code text interleaved with <BlankSlotV2> placeholders. |
successContent | ReactNode | — | Replaces the template on a fully-correct submit. |
onComplete | () => void | — | Fires on a fully correct submit. |
onWrong | () => void | — | Fires on a submit with one or more wrong blanks. |
onBlankWrong | (blankId, pickedIdx) => void | — | Fires once per wrong blank on a wrong submit. |
onBlankCorrect | (blankId) => void | — | Fires the first time the learner picks the correct option for a blank. |
successMessage | ReactNode | 'All correct!' | Banner shown on success. |
successExtra | ReactNode | — | Extra content below the success banner. |
failMessage | ReactNode | 'Not quite — check the red blanks.' | Banner shown on failure. |
submitLabel | ReactNode | 'Check' | Submit button label. |
retryLabel | ReactNode | 'Try again' | Retry button label. |
codeClassName | string | — | Extra className on the code wrapper. |
className | string | — | Extra className on the outer wrapper. |
BlankSlotV2
| Prop | Type | Default | Description |
|---|---|---|---|
blank | CodeFillV2Blank | required | The blank descriptor this slot represents. |
minWidth | number | 120 | Minimum width of the blank button (px). |
dropdownMinWidth | number | minWidth + 60 | Minimum width of the options dropdown (px). |
CodeFillV2Blank
| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Stable identifier — must be unique within the blanks array. |
label | string | required | Short label shown when the blank is empty. |
options | readonly string[] | required | Choice strings in display order. |
correctIdx | number | required | Index of the correct option in options. |
wrongFeedback | ReactNode | — | Fallback feedback shown after a wrong submit on this blank. |
optionFeedback | Partial<Record<number, ReactNode>> | — | Feedback keyed by option index — wins over wrongFeedback when present. |
Accessibility
- Each blank is a real
<button>witharia-haspopup="listbox",aria-expanded, and anaria-labelthat announces the slot's label and current value (or "not filled"). - The dropdown is a
role="listbox"whose options userole="option"and reflect selection througharia-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-visiblering (--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 —useWidgetGatefrom theLessonshell,playSound,LessonButton.Pill, and the project-specificCodeTracesuccess 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 withoutLessonButtonas a dependency, every colour routed throughcb-*tokens, every transition routed throughSPRINGS.snap/SPRINGS.smooth, and the success-state reveal made optional viasuccessContent(the original always swapped to aCodeTrace). - Companion to the Shiki-template-string variant at
/docs/forms/code-fill(extracted from AlgoFlashcards) — same submit model, different authoring surface.