Gated Code Bridge
A two-state code surface fronted by a single prediction question. The locked snippet shows first; the question and an unlock button sit beneath it. On click, the gate fires onUnlock and cross-fades the locked code into the unlocked variant — annotated lines, expanded comments, or the full output trace.
function fib(n, memo = {}) {
if (n < 2) return n;
if (memo[n] !== undefined) return memo[n];
memo[n] = fib(n - 1, memo) + fib(n - 2, memo);
return memo[n];
}Predict first
On the third call, does the cache shortcut fire — or does the recursion descend?
Installation
npx shadcn@latest add https://craftbits.dev/r/gated-code-bridge.jsonUsage
import { useState } from "react";
import { GatedCodeBridge } from "@craft-bits/core";
const [unlocked, setUnlocked] = useState(false);
<GatedCodeBridge
gateQuestion="Does the cache check fire on the third call?"
unlocked={unlocked}
onUnlock={() => setUnlocked(true)}
lockedContent={<pre><code>{plainSnippet}</code></pre>}
unlockedContent={<pre><code>{annotatedSnippet}</code></pre>}
/>Uncontrolled — let the gate flip itself on click:
<GatedCodeBridge
gateQuestion="Will the early return fire on the next call?"
onUnlock={() => trackUnlock()}
lockedContent={lockedJsx}
unlockedContent={unlockedJsx}
/>Pair with a parent reducer to chain gates:
<GatedCodeBridge
gateQuestion="Is the invariant preserved after the swap?"
unlocked={phase >= "revealed"}
onUnlock={() => dispatch({ type: "REVEAL" })}
disabled={phase === "predict-required"}
unlockLabel="Show me"
lockedContent={lockedJsx}
unlockedContent={unlockedJsx}
/>Understanding the component
- Single-shot reveal. The unlock button disables itself once
unlockedflips totrue. There is no "re-lock" — the gate is a one-way commitment. Wireunlocked={false}from the parent to reset. - Parent or self. Pair
unlockedwithonUnlockto lift the flag into a parent reducer for scoring or persistence. Skipunlockedfor a self-contained gate that tracks its own flag. - Bring your own code renderer. The gate is agnostic of how the code surfaces are rendered — pass any ReactNode for
lockedContentandunlockedContent. UseCodeBlock,CodeTrace, a plainpretag, or aMagicMoveBlockfor diff-style reveals. - Cross-fade honours reduced motion. Locked and unlocked surfaces cross-fade with a smooth spring. Under
prefers-reduced-motion: reducethe fade collapses to instant — no Y-axis slide, no spring. - Live region for screen readers. The code surface is wrapped in
aria-live="polite"so the unlocked variant is announced when the swap happens. The unlock button carriesaria-expandedandaria-controlspointed at the surface so AT announces the bridge as a disclosure.
Variants
- Predict-first chain — pair with
BinaryPredictionGateupstream and gate the unlock button viadisableduntil the prediction lands. - Annotation reveal — show the same code in both surfaces, but tint the answer line and pin a margin note in the unlocked variant.
- Diff reveal — pass a
MagicMoveBlockasunlockedContentso the swap animates a code diff rather than a static page.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
lockedContent | ReactNode | required | Code surface shown before unlock. |
unlockedContent | ReactNode | required | Code surface shown after unlock. |
gateQuestion | ReactNode | required | One-sentence prediction prompt rendered above the unlock button. |
onUnlock | () => void | — | Fired once when the student clicks the unlock button. |
unlocked | boolean | — | Controlled unlocked flag. Pair with onUnlock. |
unlockLabel | ReactNode | "Reveal" | Button label before unlock. |
gateLabel | ReactNode | "Predict first" | Header label rendered above the gate question. |
disabled | boolean | false | Force-disable the unlock button without revealing. |
aria-label | string | "Gated code reveal" | Accessible name for the gate region. |
className | string | — | Merged onto the outer <div>. |
Accessibility
- The root is a
role="group"labelled by the gate question. The code surface carriesaria-live="polite"so the unlocked variant is announced when the cross-fade swaps. - The unlock button carries
aria-expandedandaria-controlspointed at the surface id, surfacing the bridge as a native disclosure for assistive tech. - The button clears the 44 × 44 px minimum touch target via
min-h-[44px]+ horizontal padding. - Focus-visible ring uses
--cb-accentfor keyboard focus. - Tap feedback and cross-fade both collapse to instant under
prefers-reduced-motion: reduce.
Credits
- Extracted from:
algoflashcards(src/lessons/primitives/interaction/GatedCodeBridge.tsx). The source threadedCodeTrace,StepCaption, per-section toggle buttons, a viewed-sections ref, and lesson sound effects to gate an internal "Complete" button on full exploration. The craft-bits extract distills the API to its kernel — a single binary reveal withlockedContent+unlockedContent+gateQuestion+onUnlock— drops the sectioned exploration mode in favour of upstream composition withBinaryPredictionGateorCodeFill, swaps the per-track hex through--cb-accent, and routes every transition throughSPRINGS.smooth/SPRINGS.snapso consumers pick up theme + motion changes without touching the gate.