Lesson

A reusable page shell for any lesson, walkthrough, or chaptered article. Breadcrumb plus animated title and subtitle, an optional chrome slot for sticky progress rails or mode toggles, the phase column for the lesson body, and an optional footer slot for next-lesson navigation — every region can be toggled off via prop, so the same primitive serves a minimal quick-reference page, a fully-scaffolded six-phase walkthrough, and a celebration screen at the end.

Two pointers

Sliding windows, monotone invariants, and the linear-scan rule.

Progress1 / 3

Setup

Two indices, one moving from each end. The invariant lives in the gap between them.

The invariant

Each step shrinks the window by one. The total work is linear.

Practice

Try the pattern on a sorted array and a target sum.

Customize
Slots

Installation

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

Usage

import { Lesson } from "@craft-bits/edu";
 
<Lesson
  breadcrumb={<Breadcrumb trail={[{ label: "Docs", href: "/" }, { label: "Hashing" }]} />}
  title="Two pointers"
  subtitle="Sliding windows, monotone invariants, and the linear-scan rule."
  header={<LessonProgressBar value={progress} />}
  footer={<LessonNav onNext={goNext} />}
>
  <section>Phase one prose</section>
  <section>Phase two prose</section>
</Lesson>

Drop the breadcrumb by omitting the breadcrumb prop. Skip the header chrome by omitting header. Push any kind of progress rail, sticky CTA, share dock, or next-lesson banner into the matching slot — the shell never dictates their shape.

Understanding the component

  1. Breadcrumb row. Rendered first when breadcrumb is non-null. The shell wraps your trail in a plain flex row but never paints it — the prop is a ReactNode, so a project's own breadcrumb component lands verbatim.
  2. Header. The title renders as the page h1 at the top of the sans-serif scale with text-wrap: balance. The optional subtitle sits below in cb-fg-muted with text-wrap: pretty. Both fade-and-rise in on mount with SPRINGS.smooth.
  3. Header slot. Anything passed to header renders between the title and the phase column. Sticky progress bars, mode toggles, lesson chrome live here. The slot is the consumer's; its motion and chrome are their responsibility.
  4. Phase column. Children render in a single vertical-rhythm column with gap-10 between siblings. Phases own their own headings and motion — the shell just arranges them.
  5. Footer. Anything passed to footer renders inside a footer landmark below the phase column. Use it for next-lesson CTAs, share controls, completion summaries.
  6. Reduced motion. useReducedMotion() short-circuits every entry animation to its final pose.

Variants

Minimal shell — title only

<Lesson title="Quick reference">
  <pre>{snippet}</pre>
</Lesson>

Sticky progress rail in the header slot

<Lesson
  title="Backpropagation, in pictures"
  subtitle="A six-phase walk from forward pass to weight update."
  header={
    <div className="sticky top-0 z-10">
      <PhaseRail phases={phases} active={activeId} />
    </div>
  }
  footer={<CompletionScreenTemplate title="Done!" cta={{ label: "Next" }} />}
>
  {phases.map((p) => <PhaseSection key={p.id} {...p} />)}
</Lesson>

Lesson with a final completion screen as footer

<Lesson
  breadcrumb={trail}
  title="LoRA, end to end"
  footer={
    <CompletionScreenTemplate
      title="You finished LoRA"
      stats={stats}
      cta={{ label: "Continue" }}
      onContinue={advance}
    />
  }
>
  {/* phases */}
</Lesson>

Props

Lesson

PropTypeDefaultDescription
titleReactNodePage h1 rendered above the phase column.
subtitleReactNodeOptional sub-headline in muted color.
breadcrumbReactNodeOptional breadcrumb / trail rendered above the title.
headerReactNodeOptional chrome slot rendered between the header and phase column.
footerReactNodeOptional trailing slot rendered inside a footer landmark.
childrenReactNodePhase column body. Children render in a vertical-rhythm column.
regionAriaLabelstringOptional aria-label for the outer article landmark.
classNamestringMerged onto the root article via cn().
...restHTMLAttributes<HTMLElement>Any other element attribute.

Accessibility

  • The shell renders an article landmark so screen readers expose the lesson as a self-contained region. Pass regionAriaLabel to override the default name.
  • Header renders as a single h1 with text-wrap: balance for tidy multi-line titles. The subtitle uses text-wrap: pretty for relaxed body wrapping.
  • The footer slot is wrapped in a semantic footer landmark.
  • Title and subtitle entry animations respect prefers-reduced-motion — every spring drops to the final pose.
  • Color contrast: subtitle uses --cb-fg-muted (4.5:1 on --cb-bg); the breadcrumb / footer chrome inherit colours from their slot contents.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/chrome/Lesson.tsx). The original was a tightly bound compound — LessonRoot, LessonPhase, LessonRail, LessonProgressBar — that tracked active phases via scroll, drove a sticky rail, piped telemetry into Convex, and surfaced widget gates. craft-bits strips every curriculum-specific concern (slug, version, lessonId, phase tracking, gating, analytics) and keeps the underlying intent: arrange a breadcrumb, an animated title plus subtitle, an optional chrome slot, the phase column, and an optional footer.