Demo Sandbox

A bordered card built to host one interactive teaching demo. It gives <Annotation> and <MeasureLine> a position: relative body to anchor against, and it mounts an AnnotationProvider so every annotation inside can fade out together via the annotationsVisible prop — turning a teaching frame into a clean static screenshot without unmounting any DOM.

Array
7head
2
9
4
1tail
n = 5

Two annotations, one measure-line.

Customize
Visibility

Installation

npx shadcn@latest add https://craftbits.dev/r/demo-sandbox.json

Usage

import { DemoSandbox, Annotation, MeasureLine } from "@craft-bits/core";
 
<DemoSandbox title="Array">
  <YourDemo />
  <Annotation value="head" at="top-left" connector="line" />
  <MeasureLine direction="horizontal" length={200} label="n = 5" />
</DemoSandbox>

Toggle every <Annotation> inside at once — useful for a "show / hide annotations" switch above the demo:

const [annotated, setAnnotated] = useState(true);
 
<DemoSandbox annotationsVisible={annotated}>
  <YourDemo />
  <Annotation value="1" at="top-center" connector="line" />
  <Annotation value="2" at="bottom-center" connector="line" />
</DemoSandbox>

Understanding the component

  1. Positioned body. The inner body is position: relative so annotations and measure-lines drop pixel-perfect overlays without each consumer wiring up its own anchor element.
  2. Group visibility via context. DemoSandbox mounts an AnnotationProvider. Every <Annotation> deep inside reads the same shared value via useAnnotation() and animates its opacity in sync — no per-annotation prop drilling.
  3. No content discrimination. Unlike the source DemoSandbox (which sliced children by child.type), this version is a pure layout shell. Annotations and your demo are siblings — the call-site stays declarative and the component has no hidden slot system.
  4. Soft floor on body height. The body has a min-h-[12.5rem] so very-short demos still feel deliberate inside the card. Pass bodyClassName to override.
  5. Caption slot. An optional caption prop renders a small mono note below the card — the canonical place for a one-line takeaway under a teaching frame.

Props

PropTypeDefaultDescription
titlestringHeader label — rendered uppercase mono in the top bar.
annotationsVisiblebooleantrueToggle every <Annotation> inside at once.
captionReactNodeCaption rendered below the card.
bodyClassNamestringClass merged onto the positioned body.
classNamestringClass merged onto the outer wrapper.
...restHTMLAttributes<HTMLDivElement>Any other <div> prop.

Accessibility

  • The outer wrapper carries data-cb-demo-sandbox so consumers can target it for app-level styling. It has no implicit role — the demo inside is responsible for its own ARIA.
  • The optional title is a plain text label, not a heading — the docs page outline is owned by the surrounding article. Pass an aria-label directly if your demo needs landmark semantics.
  • The group visibility toggle (annotationsVisible) is reflected purely through the shared context — annotations animate their opacity so screen readers still see the same DOM structure either way. Pair with a visible toggle control above the demo so sighted users have a way to flip the state.
  • Color contrast in the default theme: the header label uses --cb-fg-muted and the caption uses --cb-fg-muted — both pass WCAG AA against --cb-bg-elevated.

Credits

  • Extracted from: terminal-dreams (src/components/principles/demo-primitives/DemoSandbox.tsx). The original used Children.forEach + child.type slot discrimination (Controls / Tabs / Caption) and a CSS-module-driven dot grid; craft-bits drops the slot machinery so the call-site stays a flat tree of siblings — the demo, its annotations, and its measure-lines all sit as direct children. The caption survives as an explicit prop because it's purely presentational.