Skip to content

Controlled Mode

useZoomPinch supports both controlled and uncontrolled modes, just like native React inputs.

The hook manages state internally. You read it via view and change it via imperative methods:

const { view, setView, resetView } = useZoomPinch({ containerRef })
// view is read-only, updated by gestures or imperative methods

Use initialViewState to set the starting position:

const { view } = useZoomPinch({
containerRef,
initialViewState: { x: 100, y: 200, zoom: 2 },
})

Pass viewState and onViewStateChange to take full control:

import { useRef, useState } from "react"
import { useZoomPinch, type ViewState } from "use-zoom-pinch"
function ControlledCanvas() {
const containerRef = useRef<HTMLDivElement>(null)
const [viewState, setViewState] = useState<ViewState>({ x: 0, y: 0, zoom: 1 })
const { view } = useZoomPinch({
containerRef,
viewState,
onViewStateChange: setViewState,
})
return (
<div ref={containerRef} style={{ overflow: "hidden", touchAction: "none" }}>
<div
style={{
transform: `translate(${view.x}px, ${view.y}px) scale(${view.zoom})`,
transformOrigin: "0 0",
}}
>
{/* content */}
</div>
</div>
)
}

Save to localStorage, URL params, or a database:

const [viewState, setViewState] = useState<ViewState>(() => {
const saved = localStorage.getItem("canvasView")
return saved ? JSON.parse(saved) : { x: 0, y: 0, zoom: 1 }
})
// Save on change
const handleChange = (view: ViewState) => {
setViewState(view)
localStorage.setItem("canvasView", JSON.stringify(view))
}
useZoomPinch({
containerRef,
viewState,
onViewStateChange: handleChange,
})

Two containers showing the same content at the same position:

const [viewState, setViewState] = useState<ViewState>({ x: 0, y: 0, zoom: 1 })
// Both canvases share the same view
const canvas1 = useZoomPinch({ containerRef: ref1, viewState, onViewStateChange: setViewState })
const canvas2 = useZoomPinch({ containerRef: ref2, viewState, onViewStateChange: setViewState })

Intercept and modify updates before applying:

const handleChange = (view: ViewState) => {
// Snap zoom to 25% increments
const snappedZoom = Math.round(view.zoom * 4) / 4
setViewState({ ...view, zoom: snappedZoom })
}

Since viewState is React state, it triggers re-renders for dependent UI:

<div>
Position: ({viewState.x.toFixed(0)}, {viewState.y.toFixed(0)}) Zoom:{" "}
{Math.round(viewState.zoom * 100)}%
</div>

Imperative methods (setView, zoomIn, etc.) work in controlled mode too — they call your onViewStateChange:

const { zoomIn, resetView } = useZoomPinch({
containerRef,
viewState,
onViewStateChange: setViewState,
})
// These call setViewState under the hood
<button onClick={() => zoomIn(1.5, { animate: true })}>Zoom In</button>
<button onClick={() => resetView({ animate: true })}>Reset</button>