Lesson Button
A compound API for the three button shapes that lesson and tutorial UIs need on repeat. Pill is the action button (Continue, Reset, Submit), Decision is a side-by-side pair for binary choices, Quiz is a vertical answer list with optional reveal feedback. All three sub-parts share a single CVA recipe so visual rhythm stays consistent across the trio.
Customize
Shape
pill
Quiz state
Installation
npx shadcn@latest add https://craftbits.dev/r/lesson-button.jsonUsage
import { LessonButton } from "@craft-bits/core";
<LessonButton.Pill tone="accent">Continue</LessonButton.Pill>
<LessonButton.Decision
leftLabel="Include"
rightLabel="Exclude"
onValueChange={setChoice}
/>
<LessonButton.Quiz
options={[
{ value: "a", label: "O(n log n)" },
{ value: "b", label: "O(n)" },
]}
correctValue="b"
revealed={revealed}
onValueChange={setPick}
/>Understanding the component
- Shared CVA recipe. All three parts use one
lessonButtonVariantsrecipe —tone(default / accent / success / error) andsizemap to the samecb-*semantic tokens that the baseButtonuses. The trio re-skins together when a consumer overrides--cb-accent(or any other token). Pill— Radix slot + Motion. Defaults to amotion.buttonwithwhileTap=TAP_SCALEandtransition=SPRINGS.snap. SettingasChildswitches to a static RadixSlotso the parent can supply an anchor (or any element) while keeping the styling. Theloadingprop renders an inline spinner and setsaria-busy.Decision— radiogroup + roving tabindex. Two equal-flex halves separated by a 1-px decorative divider. The selected side fills withcb-accent; the inactive side stayscb-bg-elevated. Arrow Left/Up commits the left side, Arrow Right/Down commits the right; Home / End jump to the ends. Only the selected (or first, when unset) button keeps tabIndex=0.Quiz— radiogroup + reveal feedback. Each option is a full-widthmotion.buttoninsiderole="radiogroup". The state recipe walks four mutually-exclusive cases in priority order: correct, wrong, selected, default. Whenrevealedistrue, the option matchingcorrectValuepaintscb-success; if the user's pick differs, their wrong choice paintscb-error. After reveal, the list is inert and the active focus ring is dismissed so correctness reads cleanly.- Tap response is library-canonical. Every animated sub-part uses
TAP_SCALE(0.96) andSPRINGS.snapfrom@craft-bits/core/motion— no inlinewhileTapscales, no hand-rolled spring objects.
Variants
// Pill tones
<LessonButton.Pill tone="default">Reset</LessonButton.Pill>
<LessonButton.Pill tone="accent">Continue</LessonButton.Pill>
<LessonButton.Pill tone="success">Submit</LessonButton.Pill>
<LessonButton.Pill tone="error">Delete</LessonButton.Pill>
// Pill sizes
<LessonButton.Pill size="sm">Small</LessonButton.Pill>
<LessonButton.Pill size="md">Medium</LessonButton.Pill>
<LessonButton.Pill size="lg">Large</LessonButton.Pill>
// Decision (controlled)
<LessonButton.Decision
leftLabel="Yes"
rightLabel="No"
value={side}
onValueChange={setSide}
/>
// Quiz with reveal
<LessonButton.Quiz
options={options}
value={pick}
onValueChange={setPick}
revealed={revealed}
correctValue="b"
/>Props
LessonButton.Pill
| Prop | Type | Default | Description |
|---|---|---|---|
tone | 'default' | 'accent' | 'success' | 'error' | 'default' | Visual tone — neutral, brand, or semantic. |
size | 'sm' | 'md' | 'lg' | 'md' | Height + padding scale. |
loading | boolean | false | Show a spinner and disable interaction. Sets aria-busy. |
asChild | boolean | false | Render content into the parent via Radix Slot. |
disabled | boolean | false | Disables the button and drops the tap gesture. |
className | string | — | Merged onto the rendered element. |
...rest | HTMLMotionProps<'button'> | — | Any other motion.button prop. |
LessonButton.Decision
| Prop | Type | Default | Description |
|---|---|---|---|
leftLabel | ReactNode | required | Left button content. |
rightLabel | ReactNode | required | Right button content. |
value | 'left' | 'right' | null | — | Controlled selection. null = nothing selected. |
defaultValue | 'left' | 'right' | null | null | Uncontrolled initial selection. |
onValueChange | (value: 'left' | 'right') => void | — | Fired on selection. |
disabled | boolean | false | Disable the entire pair. |
aria-label | string | — | Accessible name for the radiogroup. |
className | string | — | Merged onto the rendered <div role="radiogroup">. |
LessonButton.Quiz
| Prop | Type | Default | Description |
|---|---|---|---|
options | readonly LessonButtonQuizOption[] | required | Answer choices. Order is preserved. |
value | string | — | Controlled selection. Pair with onValueChange. |
defaultValue | string | — | Uncontrolled initial selection. |
onValueChange | (value: string) => void | — | Fired when the user picks an option. |
revealed | boolean | false | Freeze the list and paint correctness. |
correctValue | string | — | The value considered correct. Required when revealed. |
disabled | boolean | false | Disable every option. |
aria-label | string | — | Accessible name for the radiogroup. |
className | string | — | Merged onto the rendered <div role="radiogroup">. |
Accessibility
Pillrenders a real<button>— focusable, fires on Enter / Space, participates in form submission. Providesaria-busywhenloading. Focus ring usesvar(--cb-accent)with a 2-px offset againstcb-bg.Decisionis arole="radiogroup"with tworole="radio"buttons and roving tabindex. Arrow Left/Up commits the left half; Arrow Right/Down commits the right; Home / End jump to the ends.Quizis arole="radiogroup"with onerole="radio"per option. Arrow Up/Down (and Left/Right) move focus through the list; Home / End jump to the first / last. Afterrevealed, options become inert and the focus ring is dismissed.- Color contrast: active fills sit on
cb-accent/cb-success/cb-erroragainstcb-accent-fg; muted states usecb-fgoncb-bg-elevated. All pairs pass WCAG AA in the default theme. - Motion respects
prefers-reduced-motion:motioncollapseswhileTapto an instant snap and color transitions stay sub-300 ms (duration-150). - Minimum hit area: every interactive sub-part is
min-h-[2.75rem](44 px), meeting WCAG 2.5.8.
Credits
- Extracted from:
algoflashcards(src/lessons/primitives/chrome/LessonButton.tsx). The original wired in a project-specificplaySoundand atrackHexprop for per-lesson tinting. craft-bits generalizes the three shapes (pill / decision / quiz), re-paints them through thecb-*token system, drops the sound coupling, and adds full keyboard navigation + reveal feedback for the quiz.