Skip to content

The Drag Handle

The drag handle is the only interactive element you control directly. useSplitView doesn’t render it — it just gives you a bundle of event handlers via handleProps that you spread onto whatever element you build.

handleProps contains six event handlers:

{
onPointerDown: (e: React.PointerEvent) => void
onPointerMove: (e: React.PointerEvent) => void
onPointerUp: (e: React.PointerEvent) => void
onPointerCancel: (e: React.PointerEvent) => void
onMouseEnter: () => void
onMouseLeave: () => void
}

When you spread them onto an element, you get:

  • Pointer capture — the handle tracks the cursor even when it leaves the element, so fast drags don’t lose sync
  • Automatic zoom/pan lock on pointerdown — wheel and pinch gestures are suspended during the drag to prevent conflicts
  • Hover lock via onMouseEnter / onMouseLeave — zoom/pan lock while the cursor is idle over the handle, even before a drag starts
  • Correct split updates based on pointer position in the container, clamped to [0, 100]
  • Touch-safe — pointer events handle both mouse and touch uniformly, and stopPropagation prevents pan-under-handle

A plain 2-pixel vertical line is enough:

<div
{...handleProps}
style={{
position: "absolute",
top: 0,
bottom: 0,
left: `${split}%`,
width: 24, // wider hit area
transform: "translateX(-50%)",
cursor: "col-resize",
zIndex: 10,
}}
>
<div
style={{
width: 2,
height: "100%",
margin: "0 auto",
backgroundColor: "white",
boxShadow: "0 0 4px rgba(0,0,0,0.5)",
}}
/>
</div>
<div
{...handleProps}
style={{
position: "absolute",
top: 0,
bottom: 0,
left: `${split}%`,
width: 32,
transform: "translateX(-50%)",
cursor: "col-resize",
zIndex: 10,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<div
style={{
position: "absolute",
top: 0,
bottom: 0,
width: 2,
background: "white",
boxShadow: "0 0 8px rgba(0,0,0,0.5)",
}}
/>
<div
style={{
width: 32,
height: 32,
borderRadius: "50%",
background: "white",
boxShadow: "0 2px 12px rgba(0,0,0,0.4)",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 14,
}}
>
</div>
</div>

Same idea, rotated:

<div
{...handleProps}
style={{
position: "absolute",
left: 0,
right: 0,
top: `${split}%`,
height: 24,
transform: "translateY(-50%)",
cursor: "row-resize",
zIndex: 10,
}}
>
<div
style={{
width: "100%",
height: 2,
background: "white",
boxShadow: "0 0 4px rgba(0,0,0,0.5)",
}}
/>
</div>

split is plain state, so you can use CSS transitions on the left / top property:

<div
{...handleProps}
style={{
position: "absolute",
top: 0,
bottom: 0,
left: `${split}%`,
width: 24,
transform: "translateX(-50%)",
transition: isDraggingRef.current ? "none" : "left 0.3s ease-out",
cursor: "col-resize",
}}
/>

Use setSplit(value) from the hook return:

const { setSplit } = useSplitView()
<button onClick={() => setSplit(50)}>Center</button>
<button onClick={() => setSplit(0)}>Full After</button>
<button onClick={() => setSplit(100)}>Full Before</button>

To disable split dragging entirely but keep zoom/pan, don’t render the handle (or render a non-interactive copy without handleProps spread) and use setSplit to control the split programmatically. The view state comes from the panes themselves — dragging inside a pane still pans and zooms as usual.