Lesson Display Mode
A focused radiogroup picker for "how does this lesson render?" preferences. Two to three options is the intended shape — each row carries a label, an optional one-line description, and an optional leading icon. The selected option gets an accent border, a tinted background, and a 3px rail that morphs between rows via a shared layoutId.
Reach for it when a lesson or problem-explainer needs a settings-panel toggle for content density — continuous-stack vs block-paginated, hint-on vs hint-off, narrate vs silent — and you want the option's description visible at decision time rather than buried in a tooltip.
Installation
npx shadcn@latest add https://craftbits.dev/r/lesson-display-mode.jsonUsage
import { LessonDisplayMode } from "@craft-bits/edu";
<LessonDisplayMode
aria-label="Lesson display mode"
modes={[
{ value: "continuous", label: "Continuous", description: "Sections stack as you reveal them." },
{ value: "block", label: "Block", description: "Only the current section is visible." },
]}
defaultValue="continuous"
onChange={persist}
/>Pair with a leading icon when the picker sits in a denser settings sheet:
<LessonDisplayMode
aria-label="Render style"
modes={[
{ value: "scroll", label: "Scroll", icon: <ScrollIcon /> },
{ value: "swipe", label: "Swipe", icon: <SwipeIcon /> },
]}
orientation="horizontal"
/>Anatomy
- Root —
role="radiogroup",flex flex-col gap-2(orflex-rowin horizontal mode). Spreads unknown props onto a<div>. - Row —
role="radio"<button>, accent border + tinted background when selected, hover-darkened border otherwise. Roving tabindex keeps Tab to one stop per picker. - Accent rail — a
motion.spanwith a sharedlayoutIdscoped per-instance viauseId(). Morphs between rows onSPRINGS.smooth; teleports underprefers-reduced-motion: reduce. - Icon badge — optional 9x9 leading badge that recolors with selection state —
cb-accentoncb-bgwhen on,cb-fg-mutedoncb-bg-muted/60when off. - Label + description —
cb-fglabel on top,cb-fg-muteddescription below. The description truncates with…in horizontal orientation so rows never overrun their siblings. - Check glyph — a small filled-circle indicator on the trailing edge of every row — outlined when off,
cb-accentfilled with acb-accent-fgdot when on.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modes | readonly LessonDisplayModeOption[] | required | Available modes. Order is preserved. |
value | string | — | Controlled selected mode value. Pair with onChange. |
defaultValue | string | modes[0].value | Uncontrolled initial selected value. |
onChange | (value: string) => void | — | Fired when the learner picks a new mode. |
disabled | boolean | false | Disable every option. Individual option.disabled still wins per row. |
orientation | 'vertical' | 'horizontal' | 'vertical' | Layout direction. Vertical wraps descriptions; horizontal truncates them. |
aria-label | string | — | Accessible label for the picker group. Required when no visible label. |
className | string | — | Merged onto the root via cn(). |
LessonDisplayModeOption fields: value: string, label: ReactNode, description?: ReactNode, icon?: ReactNode, disabled?: boolean.
Accessibility
- The root is a W3C
radiogroup; each row isrole="radio"witharia-checked. Passaria-label(oraria-labelledby) so screen readers announce the group's purpose. - Keyboard: Tab lands on the selected (or first-enabled) option via roving tabindex. Arrow Up/Left and Arrow Down/Right cycle through enabled options and select on the way. Home / End jump to the first / last enabled option. Space activates the focused option without scrolling the page.
- Focus: every row carries a 2px
cb-accentring on:focus-visible, with acb-bgoffset so the ring stays visible on every supported theme. - Motion: the only animated properties are
transform(the tap scale) and thelayoutId-driven rail position — neverwidth/height/top/left.prefers-reduced-motion: reduceteleports the rail instead of sliding. - Contrast: the selected state uses
cb-accentoncb-accent-muted/60, which clears WCAG AA at every supplied theme. The check glyph is acb-accent-fgdot so it remains visible on the filled accent badge.
Credits
- Extracted from:
algoflashcards(src/platform/ui/LessonDisplayMode.tsx). The source paired a hardcoded two-mode union ('continuous' | 'block') with alocalStorage-backed context provider, no visible chrome, and no keyboard handling — it shipped the mode state but not the picker UI. craft-bits' version drops the persistence concern entirely (the parent ownsvalue/defaultValue), generalises to an arbitrarymodesarray with label + description + icon per row, adds full radiogroup keyboard navigation, and ships a sliding accent rail driven bymotion'slayoutIdso the selection change feels like one continuous gesture.