Hint Ladder
A progressive hint affordance for lesson and quiz primitives. Each tap of the reveal button surfaces the next rung in the ladder — nudge first, narrow next, specific last. The ladder is sequentially locked so a learner cannot jump to the near-answer rung without spending the gentler rungs first; the cost forces deliberate thought before asking for help.
Preview
Customize
Options
Installation
npx shadcn@latest add https://craftbits.dev/r/hint-ladder.jsonUsage
import { HintLadder, type HintLadderHint } from "@craft-bits/core";
const HINTS: ReadonlyArray<HintLadderHint> = [
{ id: "nudge", level: "nudge", text: "Which pointer restores the invariant?" },
{ id: "narrow", level: "narrow", text: "The right pointer grew the window. Which side should shrink?" },
{ id: "specific", level: "specific", text: "Contract from the left while the sum exceeds the target." },
];
<HintLadder hints={HINTS} defaultRevealedLevels={0} />Controlled — lift the count into your own state to drive scoring or analytics:
const [revealed, setRevealed] = useState(0);
<HintLadder
hints={HINTS}
revealedLevels={revealed}
onRevealedLevelsChange={(next, level) => {
setRevealed(next);
trackHintReveal(level);
}}
scored
/>Anatomy
- Revealed rung list — top of the ladder. Each rung shows its level label (Nudge / Narrow / Specific) and the hint body. Already-revealed rungs stay visible so the learner can re-read earlier hints.
- Reveal button — a small lightbulb pill with the next rung's progress counter. Hides itself once every rung has been revealed.
- Cost chip — an optional "−5%" annotation surfaced via the
scoredprop. Purely presentational; the parent owns any score deduction. - Exhausted marker — a faint "All N hints revealed" caption replaces the button once the ladder is fully spent.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
hints | ReadonlyArray<HintLadderHint> | — | Ordered list of progressive hints. Each rung needs id, level, and text. |
revealedLevels | number | — | Controlled count of revealed rungs. Pair with onRevealedLevelsChange. |
defaultRevealedLevels | number | 0 | Uncontrolled starting count. |
onRevealedLevelsChange | (next: number, level: HintLadderLevel) => void | — | Fires whenever the learner reveals another rung. |
scored | boolean | false | Surface a faint "−5%" cost chip on the reveal button. Presentational only. |
initialRevealLabel | ReactNode | "Need a hint" | Label rendered before any rung is revealed. |
nextRevealLabel | ReactNode | "Next hint" | Label rendered after the first rung. |
accentColor | string | warning token | Override accent color for the rung labels and reveal-button glyph. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- The wrapper exposes
role="group"and a defaultaria-labelof "Progressive hint ladder" so screen readers can navigate the ladder as a unit. - Each newly revealed rung is announced via
aria-live="polite"so screen readers convey the rung as it appears without interrupting in-flight speech. Already-revealed rungs flip toaria-live="off"so they are not re-announced on subsequent reveals. - The reveal button enforces a 44 px minimum hit area and shows a visible
:focus-visiblering keyed to the accent token. - The button's
aria-labelfollows a "Reveal hint N of M — Level" template so screen readers convey progress and rung semantics even when the visible label is a generic prompt. - Color is never the only channel — the rung-level label (Nudge / Narrow / Specific) is rendered as text on every revealed rung.
- Animations respect
prefers-reduced-motionvia Motion's reduced-motion handling — rungs cross-fade instantly via the sameSPRINGS.snaptransition.
Credits
- Extracted from:
AlgoFlashcards(src/lessons/primitives/interaction/HintLadder.tsx). Dropped the per-distractor map, the lesson-runtimeplaySoundimport, the workbenchControlSchema, and theRichTextbody in favour of a plain ReactNode. Lifted the counter into a controlled + uncontrolled API following the Radix pattern, replaced the project token helpers with the--cb-warningtoken plus a genericaccentColoroverride, and routed transitions throughSPRINGS.snap.