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.

Customize
State
active
Style
md

Installation

npx shadcn@latest add https://craftbits.dev/r/lesson-button.json

Usage

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 when state="active".
  • Index — optional small monospace number rendered before the label. tabular-nums so a column of stacked buttons aligns vertically. Retones to cb-accent / cb-success to match the row's state.
  • Label — primary action / step name. Truncates with truncate so 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 feedbackwhileTap: { scale: 0.98 } on SPRINGS.snap — transform-only motion so the row stays GPU-composited and never reflows. Short-circuited under prefers-reduced-motion.

Props

PropTypeDefaultDescription
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).
labelReactNoderequiredPrimary label — the action or step name.
indexReactNodeOptional small index (e.g. '01'). Rendered with tabular-nums.
hintReactNodeOptional supporting line under the label.
trailingReactNodeOptional trailing slot.
disabledbooleanfalseNative HTML disabled — removes the row from the tab order (different from state="locked").
classNamestringMerged 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" sets aria-current="step" so screen readers announce the current step on focus.
  • state="locked" sets aria-disabled="true" and drops pointer-events, without removing the row from the tab order — keyboard discoverable but announced as disabled.
  • The native disabled prop 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-2 against the page background, so it stays visible in light and dark themes.
  • Motion is transform / opacity / colour only — never width / height / top / left. prefers-reduced-motion: reduce short-circuits the tap scale entirely.
  • The minimum row height is 36px at size="sm" and 44px at size="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 compound Quiz / Pill / Decision triple coupled to playSound('correct' | 'wrong' | 'tap'), raw trackHex strings, and lessonId-driven sound de-duplication. craft-bits' edu version is intentionally narrower — a single state-aware row keyed on state with cb-* semantic tokens, no audio side effects, and idle / active / complete / locked as 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's LessonButton.