Resizable Split Pane
A split view with pointer capture, pixel minimums resolved against real width, and a separator you can actually tab to.
DOM + CSS0 deps1 file · 5 KB TSX
Props
"use client";
import { useRef, useState, type ReactNode } from "react";
export function ResizableSplitPane({
first,
second,
initial = 46,
minPx = 140,
orientation = "horizontal",
step = 2,
onResize,
}: {
first?: ReactNode;
second?: ReactNode;
/** Starting size of the first pane, as a percentage. */
initial?: number;
/** Minimum size of EITHER pane, in pixels. */
// ---------------------------------------------------------------
// Full source available with a viberdy Pro subscription.
// https://viberdy.dev/pricing
// ---------------------------------------------------------------
}Details
About this component
A split pane is four lines of flexbox and four bugs. The first is that a drag handled with document-level move listeners, or worse with listeners on the handle itself, dies the moment the pointer outruns a six-pixel target — which on any fast drag is immediately; pointer capture fixes it in one line and removes the need for document listeners at all, so there is nothing left to leak. The second is the clamp: minimum sizes are naturally written in pixels while the split itself is a percentage, and clamping the percentage against a fixed number lets a pane collapse under its minimum in a narrow container while refusing to reach it in a wide one, so the bound has to be recomputed from the measured size on every move. The third is that the separator is almost never a real control — here it carries the separator role with a current value, takes focus, moves on arrow keys, jumps to the extremes on Home and End and restores the default on Enter, because a split that can only be dragged is unusable by keyboard. And the fourth is invisible until content gets wide: flex items default to a minimum size of auto, so without an explicit zero minimum a wide child silently refuses to shrink and the panes stop matching the percentage they are told to use.
Curator’s note
Every resizer that only works if you drag slowly is missing setPointerCapture. It is one line.


