Skip to content

Infinite Canvas

An infinite canvas is the most common use case for useZoomPinch — think Figma, Miro, or Excalidraw.

Infinite Canvas
100%
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>
)
}
  1. position: "absolute" on the transformed layer — prevents the content from affecting container layout.

  2. pointerEvents: "none" on the grid — allows gestures to pass through to the container.

  3. Large enough grid extent — make it big enough that users can’t easily pan past the edges. Consider dynamic generation based on viewport.

  4. touchAction: "none" — essential on the container to prevent browser’s native scroll/zoom on touch devices.