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

Usage

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

  1. Single-shot reveal. The unlock button disables itself once unlocked flips to true. There is no "re-lock" — the gate is a one-way commitment. Wire unlocked={false} from the parent to reset.
  2. Parent or self. Pair unlocked with onUnlock to lift the flag into a parent reducer for scoring or persistence. Skip unlocked for a self-contained gate that tracks its own flag.
  3. Bring your own code renderer. The gate is agnostic of how the code surfaces are rendered — pass any ReactNode for lockedContent and unlockedContent. Use CodeBlock, CodeTrace, a plain pre tag, or a MagicMoveBlock for diff-style reveals.
  4. Cross-fade honours reduced motion. Locked and unlocked surfaces cross-fade with a smooth spring. Under prefers-reduced-motion: reduce the fade collapses to instant — no Y-axis slide, no spring.
  5. 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 carries aria-expanded and aria-controls pointed at the surface so AT announces the bridge as a disclosure.

Variants

  • Predict-first chain — pair with BinaryPredictionGate upstream and gate the unlock button via disabled until 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 MagicMoveBlock as unlockedContent so the swap animates a code diff rather than a static page.

Props

PropTypeDefaultDescription
lockedContentReactNoderequiredCode surface shown before unlock.
unlockedContentReactNoderequiredCode surface shown after unlock.
gateQuestionReactNoderequiredOne-sentence prediction prompt rendered above the unlock button.
onUnlock() => voidFired once when the student clicks the unlock button.
unlockedbooleanControlled unlocked flag. Pair with onUnlock.
unlockLabelReactNode"Reveal"Button label before unlock.
gateLabelReactNode"Predict first"Header label rendered above the gate question.
disabledbooleanfalseForce-disable the unlock button without revealing.
aria-labelstring"Gated code reveal"Accessible name for the gate region.
classNamestringMerged onto the outer <div>.

Accessibility

  • The root is a role="group" labelled by the gate question. The code surface carries aria-live="polite" so the unlocked variant is announced when the cross-fade swaps.
  • The unlock button carries aria-expanded and aria-controls pointed 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-accent for 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 threaded CodeTrace, 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 with lockedContent + unlockedContent + gateQuestion + onUnlock — drops the sectioned exploration mode in favour of upstream composition with BinaryPredictionGate or CodeFill, swaps the per-track hex through --cb-accent, and routes every transition through SPRINGS.smooth / SPRINGS.snap so consumers pick up theme + motion changes without touching the gate.