Skip to content

useSplitView

Headless React hook for building side-by-side comparison UIs with synchronized zoom, pan, and pinch. Zero styling opinions — you bring the markup.

Drag the handle to change the split, scroll to pan, ctrl/⌘ + scroll to zoom, pinch on a trackpad, or click the buttons above.

Headless by design

No DOM output, no CSS dependencies. Style the split, handle, and content any way you want.

Synchronized zoom/pan

Both panes share the same view state through use-zoom-pinch — pan and zoom stay in lockstep.

Horizontal & vertical

Swap between top/bottom and left/right splits with a single prop.

Fit to container

Report the content’s natural size once — the hook computes fitScale and compensates zoom on resize.

import { useSplitView } from "use-split-view"
function Comparison() {
const { containerRef, getPaneState, handleProps, setNaturalSize, split } = useSplitView()
const start = getPaneState("start")
const end = getPaneState("end")
return (
<div
ref={containerRef}
style={{
position: "relative",
width: "100%",
height: 500,
overflow: "hidden",
touchAction: "none",
userSelect: "none",
}}
>
<div style={{ position: "absolute", inset: 0, clipPath: start.clipPath }}>
<div style={{ width: "100%", height: "100%", transformOrigin: "top left", transform: start.transform }}>
<div style={start.contentStyle}>
<img
src="/before.jpg"
style={{ width: "100%", height: "100%", objectFit: "fill" }}
draggable={false}
onLoad={(e) => setNaturalSize(e.currentTarget.naturalWidth, e.currentTarget.naturalHeight)}
/>
</div>
</div>
</div>
<div style={{ position: "absolute", inset: 0, clipPath: end.clipPath }}>
<div style={{ width: "100%", height: "100%", transformOrigin: "top left", transform: end.transform }}>
<div style={end.contentStyle}>
<img
src="/after.jpg"
style={{ width: "100%", height: "100%", objectFit: "fill" }}
draggable={false}
/>
</div>
</div>
</div>
<div
{...handleProps}
style={{
position: "absolute",
top: 0,
bottom: 0,
left: `${split}%`,
width: 24,
transform: "translateX(-50%)",
cursor: "col-resize",
}}
>
<div style={{ width: 2, height: "100%", margin: "0 auto", background: "white", boxShadow: "0 0 4px rgba(0,0,0,0.5)" }} />
</div>
</div>
)
}
  • containerRef — attach to the outer element that owns the split.
  • getPaneState("start" | "end") — returns clipPath, transform, and contentStyle for one side.
  • handleProps — spread on your custom drag handle (pointer capture, zoom lock, all included).
  • Zoom/pan stateview, setView, centerZoom, resetView, displayZoomPct.
  • Fit-to-containersetNaturalSize, fitScale, displaySize for seamless content sizing.

Next: head to Getting Started for a step-by-step walkthrough.