May 2025

Resizable Panels

A single drag handle separates two panels. Pointer capture routes all move events to the handle even when the cursor leaves it. The split is clamped between 20% and 80% so neither panel collapses. On pointerup, the split snaps to the nearest 5% increment with a spring.

Resizable Panels

resizable panels
Panel A · 50%
Projects
Components
Assets
Settings
Panel B · 50%
Drag the divider. Pointer capture keeps tracking outside the handle boundary. Releases snap to the nearest 5%.

A two-panel split view where the divider can be dragged across the full width. The core mechanic is pointer capture — once the handle acquires the pointer, move events keep arriving even if the cursor leaves the element entirely.

Pointer capture

Standard mousemove listeners break if the pointer moves faster than the event rate and leaves the handle element. setPointerCapture fixes this by routing all future events for that pointer ID to the capturing element, regardless of where the cursor actually is.

const onDown = (e: React.PointerEvent) => {
  e.currentTarget.setPointerCapture(e.pointerId);
  dragging.current = true;
};

On pointerup or pointercancel, capture is automatically released.

Snap to grid

On release, the raw split percentage is rounded to the nearest 5% increment:

const nearest = Math.round(split / 5) * 5;
setSplit(nearest);

This makes it easy to hit common splits (50/50, 33/67, 25/75) without hunting. The transition back to the snap point is animated with cubic-bezier(0.23, 1, 0.32, 1) — a fast initial movement that decelerates, not a linear tween.

The split is clamped to [20%, 80%] so neither panel collapses entirely.