Infinite Canvas
An infinite canvas is the most common use case for useZoomPinch — think Figma, Miro, or Excalidraw.
Infinite Canvas
Shape A
Shape B
Shape C
import { useRef, useState } from "react"import { useZoomPinch, type ViewState } from "use-zoom-pinch"
function InfiniteCanvas() {const containerRef = useRef<HTMLDivElement>(null)const [viewState, setViewState] = useState<ViewState>({ x: 0, y: 0, zoom: 1 })
const { view, zoomIn, zoomOut, resetView } = useZoomPinch({containerRef,viewState,onViewStateChange: setViewState,minScale: 0.1,maxScale: 10,})
return (
<div style={{ width: "100vw", height: "100vh", display: "flex", flexDirection: "column" }}>{/* Toolbar */}<div style={{ padding: 8, display: "flex", gap: 8, background: "#f1f5f9" }}><button onClick={() => zoomIn(1.5, { animate: true })}>+</button><button onClick={() => zoomOut(1.5, { animate: true })}>-</button><button onClick={() => resetView({ animate: true })}>Reset</button><span>{Math.round(view.zoom \* 100)}%</span></div>
{/* Canvas container */} <div ref={containerRef} style={{ flex: 1, overflow: "hidden", touchAction: "none", cursor: "grab", background: "#fff", }} > <div style={{ transform: `translate(${view.x}px, ${view.y}px) scale(${view.zoom})`, transformOrigin: "0 0", position: "absolute", }} > <DotGrid /> {/* Your canvas content here */} </div> </div> </div>
)}function DotGrid() { const EXTENT = 5000 const SPACING = 50 const dots = []
for (let x = -EXTENT; x <= EXTENT; x += SPACING) { for (let y = -EXTENT; y <= EXTENT; y += SPACING) { dots.push( <circle key={`${x},${y}`} cx={x} cy={y} r={1.5} fill="#e2e8f0" /> ) } }
return ( <svg width={EXTENT * 2} height={EXTENT * 2} viewBox={`${-EXTENT} ${-EXTENT} ${EXTENT * 2} ${EXTENT * 2}`} style={{ position: "absolute", top: -EXTENT, left: -EXTENT, pointerEvents: "none", }} > {dots} </svg> )}// Lightweight alternative — no SVG, just CSS<div style={{ position: "absolute", inset: -5000, backgroundImage: "radial-gradient(circle, #ddd 1px, transparent 1px)", backgroundSize: "50px 50px", pointerEvents: "none", }}/>Key considerations
Section titled “Key considerations”-
position: "absolute"on the transformed layer — prevents the content from affecting container layout. -
pointerEvents: "none"on the grid — allows gestures to pass through to the container. -
Large enough grid extent — make it big enough that users can’t easily pan past the edges. Consider dynamic generation based on viewport.
-
touchAction: "none"— essential on the container to prevent browser’s native scroll/zoom on touch devices.