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.json

Usage

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

PropTypeDefaultDescription
children*ReactNodePositioned canvas content.
initialXnumber0Starting camera x offset.
initialYnumber0Starting camera y offset.
initialScalenumber0.85Starting zoom level.
onSelect(ids: string[]) => voidIds of cards inside the marquee when selection ends.
onCameraSettle(state) => voidFires after pan/zoom settles with `{ x, y, scale }`.
onHandleReady(handle) => voidExposes `animateTo` and `getCamera` for imperative control.
classNamestringOverride surface classes (e.g. to contain the viewport).