Skip to content

Zoom Controls

useSplitView exposes everything you need to build an imperative zoom toolbar: current state (view, displayZoomPct), setters (setView, centerZoom, resetView), and — through the re-exported useZoomPinch API — advanced helpers like zoomIn, zoomOut, and zoomTo.

const { view, centerZoom, resetView, displayZoomPct } = useSplitView()
return (
<div>
<button onClick={() => centerZoom(view.zoom * 1.5)}>Zoom In</button>
<button onClick={() => centerZoom(view.zoom / 1.5)}>Zoom Out</button>
<button onClick={() => resetView()}>Reset</button>
<span>{displayZoomPct}%</span>
</div>
)

centerZoom(targetZoom) zooms to the target level keeping the container center as the anchor, so the middle of what you see stays in place.

There are two useful zoom values:

  • view.zoom — the raw zoom multiplier relative to fitScale. 1 means “content fits the container exactly.”
  • displayZoomPct — a user-facing integer: round(view.zoom * fitScale * 100). This is what you typically show in the UI.

For a 3200×2000 image inside an 800×500 container, fitScale ≈ 0.25. When the user hasn’t zoomed, view.zoom = 1 but displayZoomPct = 25 — meaning “showing the image at 25% of its native size.”

<span>{displayZoomPct}%</span>
<button onClick={() => resetView()}>Reset</button>

resetView() snaps the view back to { x: 0, y: 0, zoom: 1 }.

If you want a button that shows content at its true native size (1 image pixel = 1 screen pixel), target 1 / fitScale:

const { fitScale, centerZoom } = useSplitView()
<button onClick={() => centerZoom(1 / fitScale)}>100%</button>

A discrete zoom ladder (25% / 50% / 100% / 200% / 400%):

const steps = [0.25, 0.5, 1, 2, 4]
<select
value={Math.round(view.zoom * fitScale * 100) / 100}
onChange={(e) => centerZoom(Number(e.target.value) / fitScale)}
>
{steps.map((s) => (
<option key={s} value={s}>
{s * 100}%
</option>
))}
</select>

useSplitView exposes the advanced imperative helpers of its underlying useZoomPinch instance through zoomApi. They operate on the same container that drives the panes, so you get zoomIn, zoomOut, zoomTo, panTo, panBy, fitToRect, fitToContent, zoomToElement, screenToContent, and more without spinning up a separate instance:

const { view, zoomApi } = useSplitView()
<button onClick={() => zoomApi.zoomIn()}>Zoom in</button>
<button onClick={() => zoomApi.zoomOut()}>Zoom out</button>
<button onClick={() => zoomApi.panTo(100, 50, { animate: true, duration: 250 })}>Pan to detail</button>
<button onClick={() => zoomApi.fitToRect({ x: 0, y: 0, width: 400, height: 300 })}>Fit selection</button>

centerZoom and resetView also accept AnimationOptions directly:

centerZoom(view.zoom * 2, { animate: true, duration: 250 })
resetView({ animate: true })

The re-exported easing functions (linear, easeOut, easeInOut) are available for custom AnimationOptions.easing.

Add keyboard zoom by listening on the container:

const { containerRef, view, centerZoom, resetView } = useSplitView()
useEffect(() => {
const el = containerRef.current
if (!el) return
const onKey = (e: KeyboardEvent) => {
if (e.key === "+" || e.key === "=") centerZoom(view.zoom * 1.5)
else if (e.key === "-") centerZoom(view.zoom / 1.5)
else if (e.key === "0") resetView()
}
el.addEventListener("keydown", onKey)
return () => el.removeEventListener("keydown", onKey)
}, [view.zoom, centerZoom, resetView])
// And make the container focusable:
<div ref={containerRef} tabIndex={0}>...</div>