← Back to blog

Building Resizable, Dockable Panel Layouts in React

How professional trading terminals achieve flexible multi-pane layouts — with a technical walkthrough of drag-to-resize, panel persistence, and responsive breakpoints.


Every professional trading terminal lets users arrange their workspace: drag panels to resize them, rearrange panes, and save the layout for next time. Bloomberg, Refinitiv, and most crypto exchanges offer some version of this. Traders consider it essential — they build muscle memory around their layout.

Implementing this in React is more involved than it first appears.

The layout model

A trading workspace is a recursive tree of splits and panels:

type LayoutNode =
  | { type: "panel"; id: string; component: string }
  | { type: "split"; direction: "horizontal" | "vertical"; children: LayoutNode[]; sizes: number[] };

Each split divides its space between children by percentage. Panels are the leaves — each renders a specific component (chart, order book, blotter, etc.).

A typical trading layout might be:

Horizontal Split (70/30)
├── Vertical Split (60/40)
│   ├── Panel: Chart
│   └── Panel: Order Book
└── Vertical Split (50/50)
    ├── Panel: Order Form
    └── Panel: Trade History

Resize handles

The drag-to-resize handle sits between adjacent panels in a split. It needs to:

  1. Capture mouse/touch events on the divider
  2. Calculate the new size ratio based on pointer position relative to the parent container
  3. Enforce minimum sizes to prevent panels from collapsing to zero
  4. Update the layout state with the new sizes
function onPointerMove(event: PointerEvent) {
  const parentRect = parentRef.current!.getBoundingClientRect();
  const position = direction === "horizontal"
    ? (event.clientX - parentRect.left) / parentRect.width
    : (event.clientY - parentRect.top) / parentRect.height;

  const clamped = Math.max(minSize, Math.min(1 - minSize, position));
  setSizes([clamped, 1 - clamped]);
}

Use pointer events (not mouse events) for consistent behaviour across mouse and touch. Set touch-action: none on the handle to prevent scroll interference.

Preventing layout thrashing

Resizing panels triggers re-renders of the split containers and all child panels. Without care, this causes visible jank.

Two key optimisations:

  1. CSS-based resizing — During the drag, update sizes via CSS custom properties or inline styles rather than React state. Only commit the final sizes to state on pointer up.
function onPointerMove(event: PointerEvent) {
  // Update CSS directly during drag — no React re-render
  parentRef.current!.style.setProperty("--split-position", `${position * 100}%`);
}

function onPointerUp() {
  // Commit final value to state
  const finalPosition = parseFloat(
    parentRef.current!.style.getPropertyValue("--split-position")
  );
  setSizes([finalPosition / 100, 1 - finalPosition / 100]);
}
  1. Isolate chart re-renders — Charting libraries are expensive to resize. Debounce their resize handler or use ResizeObserver with a threshold.

Persisting layouts

Traders expect their layout to survive page reloads and sessions. Serialise the layout tree to localStorage or a backend:

useEffect(() => {
  localStorage.setItem("workspace-layout", JSON.stringify(layout));
}, [layout]);

Include a version number in the serialised data so you can migrate layouts when you add new panel types or change the schema.

Default layouts and presets

Not every user wants to customise their layout. Provide sensible defaults:

  • Trading — Chart prominent, order book and form alongside, blotter below
  • Monitoring — Multiple charts in a grid, watchlist sidebar
  • Analysis — Large chart with indicator panels, minimal trading controls

Let users save their own presets and switch between them. Store presets as named layout trees.

Responsive behaviour

Trading layouts designed for a 3440px ultrawide monitor do not work on a 1366px laptop. Handle this with:

  • Minimum panel widths — Below the minimum, collapse the panel to a tab or icon
  • Breakpoint-based defaults — Different default layouts for different screen sizes
  • Tab stacking — On narrow screens, stack panels as tabs within a container rather than splitting horizontally
const isMobile = useMediaQuery("(max-width: 768px)");
const defaultLayout = isMobile ? mobileLayout : desktopLayout;

Libraries vs. building from scratch

Several libraries provide panel layout primitives:

  • react-resizable-panels — Lightweight, well-maintained. Good for basic split layouts.
  • allotment — Similar to VS Code's split view. Good performance.
  • FlexLayout — Full docking/tabbing system. Heavier but feature-complete.

For most trading applications, react-resizable-panels gives you enough to build a professional layout system without the complexity overhead of a full docking framework. You can add tab support within panels using your own component.

The CSS foundation

The split container uses flexbox with the sizes applied as flex-basis:

.split-horizontal {
  display: flex;
  flex-direction: row;
  height: 100%;
}

.split-horizontal > .panel {
  flex: 0 0 var(--panel-size);
  overflow: hidden;
}

.split-horizontal > .divider {
  flex: 0 0 4px;
  cursor: col-resize;
  background: var(--color-border);
}

.split-horizontal > .divider:hover {
  background: var(--color-accent);
}

Keep the divider narrow (3–4px) with a generous hit area (12–16px) using padding or a pseudo-element. Traders do not want thick borders consuming screen space.

Final considerations

A well-implemented panel layout system is one of the strongest signals of a professional trading application. It communicates that the platform was built for power users who spend hours in the interface. The investment in getting drag-to-resize smooth, persistence reliable, and defaults sensible pays dividends in user retention.

Kickstart Your Trading Application

Hedge UI is a React starter kit with production-ready trading components, real-time data handling, and customisable layouts — so you can ship faster.

Get Hedge UI