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
Two annotations, one measure-line.
Customize
Visibility
Installation
npx shadcn@latest add https://craftbits.dev/r/demo-sandbox.jsonUsage
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
- Positioned body. The inner body is
position: relativeso annotations and measure-lines drop pixel-perfect overlays without each consumer wiring up its own anchor element. - Group visibility via context.
DemoSandboxmounts anAnnotationProvider. Every<Annotation>deep inside reads the same shared value viauseAnnotation()and animates its opacity in sync — no per-annotation prop drilling. - No content discrimination. Unlike the source
DemoSandbox(which sliced children bychild.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. - Soft floor on body height. The body has a
min-h-[12.5rem]so very-short demos still feel deliberate inside the card. PassbodyClassNameto override. - Caption slot. An optional
captionprop renders a small mono note below the card — the canonical place for a one-line takeaway under a teaching frame.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | — | Header label — rendered uppercase mono in the top bar. |
annotationsVisible | boolean | true | Toggle every <Annotation> inside at once. |
caption | ReactNode | — | Caption rendered below the card. |
bodyClassName | string | — | Class merged onto the positioned body. |
className | string | — | Class merged onto the outer wrapper. |
...rest | HTMLAttributes<HTMLDivElement> | — | Any other <div> prop. |
Accessibility
- The outer wrapper carries
data-cb-demo-sandboxso consumers can target it for app-level styling. It has no implicit role — the demo inside is responsible for its own ARIA. - The optional
titleis a plain text label, not a heading — the docs page outline is owned by the surrounding article. Pass anaria-labeldirectly if your demo needs landmark semantics. - The group visibility toggle (
annotationsVisible) is reflected purely through the shared context — annotations animate theiropacityso 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-mutedand 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 usedChildren.forEach+child.typeslot 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. Thecaptionsurvives as an explicit prop because it's purely presentational.