Build a Resizable Panels Layout: VS Code and Linear Style

“Build a resizable panels layout” is the layout cousin of the Build-a-modal question. VS Code, Linear, and Slack all use the pattern: a sidebar you can drag wider, a main area that flexes, and sometimes a third panel that collapses. The interview probes pointer math, persistence, accessibility, and modern CSS knowledge.

Requirements to clarify

  • Two panels or three?
  • Horizontal split, vertical split, or both?
  • Min and max sizes per panel?
  • Persist sizes to localStorage?
  • Collapse fully (panel hidden) when dragged below a threshold?
  • Keyboard support for the resize handle?

The CSS foundation

Use CSS Grid with named columns. The handle is its own column with a small fixed width.

.layout {
  display: grid;
  grid-template-columns: var(--left) 6px 1fr;
  height: 100vh;
}

Drive --left from JS state. The handle is just a div in the middle column with cursor: col-resize. This avoids absolute positioning and the layout flexes naturally.

Pointer handling

On pointerdown on the handle:

  1. Capture the pointer (handle.setPointerCapture(e.pointerId)) so events keep arriving even if the user drags outside the window
  2. Record start X and start width
  3. Set document.body.style.userSelect = "none" and cursor: col-resize to prevent text selection during the drag

On pointermove: compute new width = start width + (current X – start X). Clamp to min/max. Update CSS variable.

On pointerup: release pointer capture, restore body styles, persist size to localStorage.

Why pointer events, not mouse events

Pointer events unify mouse, touch, and pen. setPointerCapture is the cleanest way to keep tracking a drag past the window edge. Mousemove on the body works but breaks on iframes and is less robust.

Min, max, and collapse

Define min and max in pixels or rem. When the user drags below a threshold (e.g., min – 30 px), snap to a fully-collapsed state (width 0, panel hidden) and keep dragging from that “collapsed” anchor. Releasing past the threshold snaps back to min.

Keyboard accessibility

The handle should be focusable and operable from the keyboard. WAI-ARIA defines the separator role for resize handles:

  • role="separator"
  • aria-orientation="vertical" (for a horizontal split)
  • aria-valuenow, aria-valuemin, aria-valuemax in pixels or %
  • Arrow keys move by a fixed step (e.g., 10 px)
  • Home/End jump to min/max
  • Enter or Space toggles collapse

Persistence

Save sizes to localStorage on drag end (not on every move — too chatty). Restore on mount; if missing, fall back to a sensible default. Watch out: SSR mismatch if you read localStorage during render. Use a useEffect or a “client-only” guard.

Performance

  • Use transform or CSS variables, not style.width directly, to avoid layout on every move
  • Throttle pointermove with requestAnimationFrame if you do expensive work in the handler
  • Containment: contain: layout style on each panel reduces reflow scope

Edge cases

  • Window resize: clamp panel sizes to new viewport
  • Touch devices: 6 px handle is too narrow; expand the hit area with a wider invisible padding
  • RTL layouts: flip drag direction
  • Inside a flex parent: ensure your grid container has a defined width or grows correctly

What separates senior from staff

Junior candidates code the drag math. Senior candidates think about the API for nesting (a panel that itself contains a split layout). Staff candidates think about the data model — sizes as a tree of weights, persistence as a serialized layout, and how the user can reset to defaults. Mentioning react-resizable-panels by Brian Vaughn (a popular OSS implementation) signals you read modern frontend code.

Frequently Asked Questions

Should I use a library?

For interview: implement from scratch. For production: react-resizable-panels is excellent and handles persistence, collapse, and keyboard out of the box.

What about iframe content inside a panel?

Pointer events from outside the iframe don’t cross the boundary, so set pointer-events: none on the iframe during a drag (and restore after). Otherwise the drag halts the moment the cursor enters the iframe.

What if I want a 3-panel layout?

Two handles, two CSS variables. Each handle resizes the panel to its left; the rightmost panel takes 1fr and absorbs the remainder.

Scroll to Top