Widget

A generic chrome container for the interactive surfaces that live inside lessons, dashboards, and explorable explanations. Wraps any body in a labelled role="region", exposes an optional actions slot for buttons or live readouts, and supports three surface tones plus an optional collapsible disclosure.

Reach for it when a lesson canvas needs a labelled card around a control surface — a dial, a slider, a small simulator — without committing to a heavier card primitive or hand-rolling the chrome each time.

Live readout

Drag the dial to rotate the basis vector. The widget body crossfades on collapse — never animates height.

Customize
Style
default

Installation

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

Usage

import { Widget } from "@craft-bits/edu";
 
<Widget title="Live readout">
  <Dial value={theta} onChange={setTheta} />
</Widget>

Render an actions row to the right of the title — useful for status pills, secondary buttons, or a live numeric readout:

<Widget
  title="Rotation"
  actions={<span className="font-mono tabular-nums">theta</span>}
>
  <Dial value={theta} onChange={setTheta} />
</Widget>

Wrap the widget in a tone wash for a hero widget on a lesson page:

<Widget title="Key intuition" tone="accent">
  <Visualisation />
</Widget>

Make the body collapsible — controlled or uncontrolled, both work:

<Widget title="Details" collapsible defaultOpen={false}>
  <Details />
</Widget>

Anatomy

  • Root<section role="region"> with the cb-bg-elevated / cb-accent-muted / cb-bg-muted surface; spreads unknown props onto the element.
  • Header — the title sits on the left as either an <h3> or a disclosure <button>; the actions slot sits on the right and stays reachable when the body is collapsed.
  • Body — wrapped in AnimatePresence with initial={false}. Animates opacity + a small translateY enter / exit on SPRINGS.smooth — never height, so the motion is GPU-composited.
  • Chevron — rotates from -90° (closed) to (open) on SPRINGS.snap.
  • Reduced motionprefers-reduced-motion: reduce short-circuits the body enter / exit and the chevron rotation; the body appears or disappears instantly.

Props

PropTypeDefaultDescription
titleReactNodeHeadline rendered in the top-left of the chrome. Omit for an anonymous container.
actionsReactNodeSlot rendered to the right of the title — buttons, pills, live readouts.
tone'default' | 'accent' | 'muted''default'Surface tone. Drives both the background wash and the title color.
collapsiblebooleanfalseRender a disclosure affordance that lets the user collapse the body.
openbooleanControlled open state. Pair with onOpenChange.
defaultOpenbooleantrueUncontrolled initial open state. Ignored when open is provided.
onOpenChange(open: boolean) => voidFires whenever the user toggles the disclosure.
childrenReactNodeThe widget body — drop any interactive surface inside.
classNamestringMerged onto the root via cn().

Accessibility

  • The root is a <section> with role="region". When title is set, an id is generated and referenced via aria-labelledby so the widget reads as a named landmark; otherwise consumers pass aria-label.
  • When collapsible, the title becomes a <button> with aria-expanded and aria-controls pointing at the body — Radix-equivalent disclosure semantics without a runtime dependency.
  • Focus styling on the disclosure trigger uses focus-visible:ring-2 ring-cb-accent ring-offset-2 ring-offset-cb-bg so keyboard focus is unambiguous on every tone.
  • Motion is opacity + transform only — never height / top / left. The body slides on translateY, the chevron rotates.
  • prefers-reduced-motion: reduce short-circuits both the body enter / exit and the chevron rotation — content appears or disappears instantly.
  • Color contrast respects the cb-* token palette so light and dark themes both clear WCAG AA.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/chrome/Widget.tsx). The source coupled the chrome to lesson curriculum — useWidgetGate, useWidgetHistory with [ / ] keybindings, eyebrow / premise / caption / formula rows wired to a project-specific narrative shape, and a mandatory onReset plus per-bookmark "canonical state" pills tied to the lesson's auto-satisfy gate. The craft-bits extract distils the API to its kernel — a labelled region with a title, an actions slot, three tones, and an optional collapsible body — and drops the curriculum coupling so the widget can wrap any interactive surface.