Canvas
The viewport. A fixed, full-bleed surface that pans, zooms (wheel + pinch),
grabs (hold space) and marquee-selects its positioned children. Place absolutely
positioned cards inside it; tag selectable ones with data-card-id.
Pan · zoom · select
Scroll or two-finger drag to pan. Pinch or ⌘/Ctrl + scroll to zoom. Drag on empty space to marquee-select cards.
Another card
And another
The preview is contained in a box. In your app the Canvas renders fixed inset-0, open the live demo for the real thing.
Installation
npx shadcn@latest add https://canvas.blode.co/r/canvas.jsonUsage
import { Canvas } from "@/components/canvas/canvas";
import { CanvasCard } from "@/components/canvas/canvas-card";
<Canvas initialScale={0.85} onSelect={(ids) => setSelected(new Set(ids))}>
<CanvasCard cardId="a" x={60} y={60}>
<div className="p-5">A card</div>
</CanvasCard>
</Canvas>;Children opt out of panning with data-no-pan, and out of selection by omitting
data-card-id. Marquee selection reports the ids of intersected [data-card-id]
elements through onSelect.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children* | ReactNode | — | Positioned canvas content. |
| initialX | number | 0 | Starting camera x offset. |
| initialY | number | 0 | Starting camera y offset. |
| initialScale | number | 0.85 | Starting zoom level. |
| onSelect | (ids: string[]) => void | — | Ids of cards inside the marquee when selection ends. |
| onCameraSettle | (state) => void | — | Fires after pan/zoom settles with `{ x, y, scale }`. |
| onHandleReady | (handle) => void | — | Exposes `animateTo` and `getCamera` for imperative control. |
| className | string | — | Override surface classes (e.g. to contain the viewport). |