Components / Structure
PanZoomCanvas
A surface to pan and zoom freely, for diagrams, maps and boards; your content, placed in canvas units.
When to useLink to "When to use"
- Free two-dimensional content bigger than the screen: diagrams, schemas, mind maps, boards.
Use something else when:
- the content is pages one under another: DocumentViewport
- it is a long list: VirtualList
UsageLink to "Usage"
<PanZoomCanvas handle={canvas} label="Schema" bounds={bounds} minimap renderMinimap={() => <Dots />}>
{nodes.map((n) => <Node key={n.id} style={{ position: "absolute", left: n.x, top: n.y }} />)}
</PanZoomCanvas>Place children absolutely in canvas units. Give bounds (the content's extent) to fit it on first show, for Shift+1 and fit(), and for the minimap. min and max set the zoom's limits in percent, as ZoomControls' do (25 to 800 unless given; a schema diagram might keep to 20 to 300): the wheel, a pinch, the keys, zoomTo and fitting all stop there. The handle has fit, zoomTo and centerOn for your own buttons (ref gives the same handle, and the earlier spelling centreOn still works); pair it with ZoomControls for a zoom toolbar.
Painting into a canvasLink to "Painting into a canvas"
An engine that draws the whole scene into a <canvas> (a large graph, a map) uses renderScreen. It gets { view, width, height, dpr, canvasProps, transform } on every view change and draws in screen pixels; a canvas point (x, y) is at (x times scale plus view.x, y times scale plus view.y). The layer sits under any children and does not take the pointer, so pressing it pans. Render your own component from it (renderScreen={(s) => <Scene {...s} />}) so its hooks belong to it.
The canvas element is yours; the screen helps size and draw it. Spread canvasProps on it for a canvas of the screen's device pixels (dpr of them per CSS pixel, so it stays sharp on dense screens) shown at the canvas's CSS size, and call ctx.setTransform(...screen.transform) to draw in canvas units. A move to another screen or a browser zoom draws again at the new dpr.
function Scene(screen: CanvasScreen) {
const ref = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const ctx = ref.current!.getContext("2d")!;
ctx.resetTransform();
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.setTransform(...screen.transform);
drawScene(ctx); // in canvas units
});
return <canvas ref={ref} {...screen.canvasProps} />;
}Dragging nodesLink to "Dragging nodes"
To drag a node (a table card) rather than pan, claim the press: claimPress is asked about every press except a Space+drag, with the point in canvas units, the element pressed and the pointer type. Return { onMove, onEnd } to follow it, true to claim it without them, or nothing to leave it to the canvas (the background and painted content pan). onMove and onEnd get { point, delta } in canvas units, whatever the zoom; onEnd also says whether Escape cancelled the drag. A press that does not travel is a tap (delta is zero); the click after a claimed press is not delivered, so select in onEnd. It serves children (look at press.target) and painted content (hit-test press.point with your own pick) alike; while two fingers pinch, the node stays put.
claimPress={({ point }) => {
const card = pick(cards, point.x, point.y);
if (!card) return; // the background pans
const from = { x: card.x, y: card.y };
return {
onMove: ({ delta }) => moveCard(card.id, from.x + delta.x, from.y + delta.y),
onEnd: ({ delta }, cancelled) => {
if (cancelled) moveCard(card.id, from.x, from.y);
else if (!delta.x && !delta.y) select(card.id);
},
};
}}Controls in the cornersLink to "Controls in the corners"
overlays places controls over the canvas in its corners: "top-start", "top-end", "bottom-start" and "bottom-end", with start and end following the page's direction. They stay put while the content pans and zooms, and keep their own pointer, wheel and keys: pressing or dragging them never pans (not even with Space held) and never reaches claimPress, and the wheel over them is theirs, though Ctrl/Cmd+wheel still zooms the canvas rather than the page. The space between a corner's controls is background. The minimap keeps the bottom-end corner, under that corner's overlay.
<PanZoomCanvas
handle={canvas}
label="Schema"
min={20}
max={300}
minimap={showMap}
overlays={{
"top-start": <Legend />,
"top-end": (
<ZoomControls zoom={zoom} percent={percent} min={20} max={300} onZoomChange={onZoomChange} />
),
"bottom-start": (
<Button size="sm" pressed={showMap} onClick={() => setShowMap((s) => !s)}>
Minimap
</Button>
),
}}
>
{cards}
</PanZoomCanvas>PropsLink to "Props"
| Prop | What it does | Type | Default |
|---|---|---|---|
labelRequired | Names the canvas ("Schema diagram"). | string | |
view | Controlled view: the pan offset and the zoom. | CanvasView | |
defaultView | Where it starts. Default: bounds fitted in view (or the origin at 100% without bounds). | CanvasView | |
onViewChange | Called with the new view on every pan and zoom. | ((view: CanvasView) => void) | |
min | The smallest zoom, in percent (as ZoomControls' min). Default 25. | number | 25 |
max | The largest zoom, in percent (as ZoomControls' max). Default 800. | number | 800 |
handle | fit, zoomTo and centerOn, for moving the view from outside (as ScrollArea's and VirtualList's handle). | Ref<PanZoomCanvasHandle> | |
bounds | The content's extent in canvas units, for fitting and the minimap. | CanvasRect | |
minimap | Show a minimap of bounds with the view on it (drag it, or press where to look). | boolean | |
renderMinimap | A small drawing of the content for the minimap, in canvas units (it is scaled down). | (() => ReactNode) | |
renderScreen | A layer in screen pixels under the content, redrawn with every view change: for an engine that paints the whole scene into a <canvas> (a canvas point (x, y) is at (x * scale + view.x, y * scale + view.y)). It does not take the pointer, so pressing it pans like the background. | ((screen: CanvasScreen) => ReactNode) | |
claimPress | Asked about each press (not one with Space held, which always pans): return handlers to follow it (drag a node, and the view stays), true to claim it without them, or nothing to leave it to the canvas (the background and painted content pan; presses on your children are left to them). Works for children and painted content alike: hit-test press.point or look at press.target. | ((press: CanvasPress) => CanvasPressHandlers | boolean | null | undefined) | |
overlays | Controls drawn over the canvas in its corners (zoom controls, a legend, a minimap toggle): they stay put while the content pans and zooms, keep their own pointer, wheel and keys, and never start a pan. The minimap keeps the bottom-end corner, under that corner's overlay. | Partial<Record<CanvasCorner, ReactNode>> | |
children | The content, placed in canvas units (absolutely positioned children). | ReactNode | |
labels | Replace the built-in English text: the zoom announcement. | Partial<PanZoomCanvasLabels> | |
className | Added to the root element, for layout. | string | |
style | Layout only (give it a height). Restyle through --cy-pan-zoom-canvas-* tokens. | CSSProperties |
Generated from the type definitions in @arshad-shah/cynosure 0.5.0.
Behaviour and accessibilityLink to "Behaviour and accessibility"
- Role: a named region and tab stop. Zoom changes are announced ("Zoom 125%").
- Pointer: drag the empty background, or hold Space and drag anywhere, to pan; the wheel pans; Ctrl/Cmd+wheel zooms around the pointer. Presses on your content are left to it, and presses
claimPressclaims never pan. - Touch: one finger pans (or drags a claimed node); two fingers pinch-zoom around their midpoint. Fingers on the corner controls are theirs.
- Focus: the overlays come after the canvas in tab order; the keys below act only while the canvas itself has focus (Ctrl/Cmd zoom keys anywhere inside).
| Key | Does |
|---|---|
| Arrows (Shift: further) | Pan |
| Ctrl/Cmd + = / - | Zoom in / out a step |
| Ctrl/Cmd + 0 | 100% |
| Shift + 1 | Fit the content |
| Space (held) + drag | Pan |
ThemingLink to "Theming"
Tokens: --cy-pan-zoom-canvas-background and --cy-pan-zoom-canvas-minimap-width.
RelatedLink to "Related"
These examples run Cynosure 0.5.0 (commit 50ce30b), the version on npm.