Lesson Button
A single state-aware button row used in lesson scaffolds. The state axis (idle, active, complete, locked) drives the full skin and the interactive affordance — locked rows become non-interactive (aria-disabled + pointer-events: none), active advertises aria-current="step", and complete retones to cb-success while staying clickable so a learner can revisit.
Reach for it when a lesson surface needs a stack of step rows — a syllabus list, a recipe outline, a milestone trail down the side of the canvas. For the compound Pill / Decision / Quiz button family, see the LessonButton in @craft-bits/core.
Installation
npx shadcn@latest add https://craftbits.dev/r/lesson-button.jsonUsage
import { LessonButton } from "@craft-bits/edu";
<LessonButton
index="02"
label="Predict the answer"
hint="3 min"
state="active"
onClick={() => goToStep(2)}
/>A full step list reads top-to-bottom as the learner progresses:
<div className="flex flex-col gap-2">
<LessonButton index="01" label="Introduce the problem" state="complete" />
<LessonButton index="02" label="Predict the answer" state="active" />
<LessonButton index="03" label="Walk the derivation" state="idle" />
<LessonButton index="04" label="Synthesis quiz" state="locked" />
</div>Anatomy
- Root — a single
motion.button.data-state="idle|active|complete|locked"is the styling hook, mirroring Radix.aria-current="step"is set only whenstate="active". - Index — optional small monospace number rendered before the label.
tabular-numsso a column of stacked buttons aligns vertically. Retones tocb-accent/cb-successto match the row's state. - Label — primary action / step name. Truncates with
truncateso a long label cannot push the trailing slot off-canvas. - Hint — optional supporting line under the label in a muted tone. Reads as the duration estimate or short description.
- Trailing — optional far-right slot for a chevron, badge, or check icon.
- Press feedback —
whileTap: { scale: 0.98 }onSPRINGS.snap— transform-only motion so the row stays GPU-composited and never reflows. Short-circuited underprefers-reduced-motion.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
state | 'idle' | 'active' | 'complete' | 'locked' | 'idle' | Visual + interaction state. locked removes pointer events and adds aria-disabled. |
size | 'sm' | 'md' | 'lg' | 'md' | Row height (36 / 44 / 56px). |
label | ReactNode | required | Primary label — the action or step name. |
index | ReactNode | — | Optional small index (e.g. '01'). Rendered with tabular-nums. |
hint | ReactNode | — | Optional supporting line under the label. |
trailing | ReactNode | — | Optional trailing slot. |
disabled | boolean | false | Native HTML disabled — removes the row from the tab order (different from state="locked"). |
className | string | — | Merged onto the root via cn(). |
All remaining button props are spread onto the root so onClick, onFocus, aria-*, data-*, etc. just work.
Accessibility
state="active"setsaria-current="step"so screen readers announce the current step on focus.state="locked"setsaria-disabled="true"and dropspointer-events, without removing the row from the tab order — keyboard discoverable but announced as disabled.- The native
disabledprop is honoured separately for callers who want the locked row skipped in the tab order entirely. - Focus ring uses
focus-visible:ring-2 focus-visible:ring-cb-accent focus-visible:ring-offset-2against the page background, so it stays visible in light and dark themes. - Motion is
transform/opacity/ colour only — neverwidth/height/top/left.prefers-reduced-motion: reduceshort-circuits the tap scale entirely. - The minimum row height is
36pxatsize="sm"and44pxatsize="md"/lg"— meeting the 40×40px hit-area guideline at the default size and above.
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/chrome/LessonButton.tsx). The source was a compoundQuiz/Pill/Decisiontriple coupled toplaySound('correct' | 'wrong' | 'tap'), rawtrackHexstrings, andlessonId-driven sound de-duplication. craft-bits' edu version is intentionally narrower — a single state-aware row keyed onstatewithcb-*semantic tokens, no audio side effects, andidle/active/complete/lockedas the only intents — sized for syllabus and milestone trails rather than inline quiz options. - The compound Pill / Decision / Quiz family from the same source lives at
@craft-bits/core'sLessonButton.