“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:
- Capture the pointer (
handle.setPointerCapture(e.pointerId)) so events keep arriving even if the user drags outside the window - Record start X and start width
- Set
document.body.style.userSelect = "none"andcursor: col-resizeto 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-valuemaxin 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
transformor CSS variables, notstyle.widthdirectly, to avoid layout on every move - Throttle pointermove with
requestAnimationFrameif you do expensive work in the handler - Containment:
contain: layout styleon 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.