Skip to content
Utilities

Resizable Panels — Component Library Guide

React components for resizable panel groups and layouts. Drag handles, keyboard resizing, collapsible panels, and persistent sizing via localStorage.

TypeScript MIT
760K weekly downloads
6.1k GitHub stars
~8kB bundle size
v2.1.7 latest version
panelslayoutresizablesplit-viewdrag

TL;DR: React Resizable Panels lets users drag to resize split-view layouts. Use PanelGroup, Panel, and PanelResizeHandle components. Supports collapsible panels, autoSaveId for persistent sizing, and full keyboard accessibility. ~8kB. Official docs: react-resizable-panels.vercel.app.

What is React Resizable Panels?

React Resizable Panels provides <PanelGroup>, <Panel>, and <PanelResizeHandle> components for building split-view layouts where the user can drag to resize sections. It's used in shadcn/ui's ResizablePanelGroup and is the standard for IDE-style split layouts in React.

The mental model is simple. A PanelGroup sets a direction, either horizontal or vertical. Inside it, Panel elements hold your content, and PanelResizeHandle elements sit between them as draggable dividers. Sizes are expressed as percentages of the group, not pixels, which is what keeps layouts fluid when the window resizes. The library does the math so two panels always add up to the full space, even mid-drag.

Why does that percentage model matter? Because pixel-based splitters break the moment the viewport changes. A sidebar fixed at 280px looks fine on a laptop and absurd on a 27-inch monitor. Percentage panels scale with the screen, and minimum and maximum bounds keep them from collapsing into something useless. It's a small design decision with a big payoff in how the layout holds up across devices.

Where split layouts earn their keep

Reach for it when building code editors and IDEs, where you want side panel, editor, and terminal splits. It fits admin dashboards too, pairing a sidebar with a content area whose width the user controls. Document viewers benefit from a file tree beside a preview pane, and email clients are the classic three-column case: folder list, message list, and message detail, each independently resizable.

The common thread is power users who spend hours in one screen. Those users have strong opinions about how much room each region gets, and a draggable splitter lets them tune the layout to their own workflow. Pair that with persistent sizing and the app remembers their preference forever, which is a quietly delightful touch that most teams forget to add.

Key Features

  • Horizontal and vertical splits - direction="horizontal" or "vertical"
  • Nested groups - panels can contain other PanelGroup components
  • Collapsible panels - collapsible + minSize for panels that snap closed
  • Persistent sizing - autoSaveId saves panel sizes to localStorage
  • Keyboard accessible - resize handles support arrow keys
  • Imperative API - panelRef.collapse() / panelRef.expand() / panelRef.resize()

Installation

npm install react-resizable-panels

Basic Usage

import { PanelGroup, Panel, PanelResizeHandle } from 'react-resizable-panels'

function SplitEditor() {
  return (
    <PanelGroup direction="horizontal" className="h-screen">
      {/* Sidebar */}
      <Panel defaultSize={20} minSize={15} maxSize={40} className="bg-slate-900">
        <FileTree />
      </Panel>

      <PanelResizeHandle className="w-1 bg-slate-700 hover:bg-indigo-500 transition-colors cursor-col-resize" />

      {/* Editor */}
      <Panel defaultSize={50} className="bg-slate-800">
        <CodeEditor />
      </Panel>

      <PanelResizeHandle className="w-1 bg-slate-700 hover:bg-indigo-500 transition-colors cursor-col-resize" />

      {/* Terminal */}
      <Panel defaultSize={30} minSize={10} className="bg-slate-900">
        <Terminal />
      </Panel>
    </PanelGroup>
  )
}

Collapsible Panel with Toggle Button

import { Panel, PanelResizeHandle, type ImperativePanelHandle } from 'react-resizable-panels'
import { useRef } from 'react'

function CollapsibleSidebar() {
  const sidebarRef = useRef<ImperativePanelHandle>(null)

  function toggleSidebar() {
    const panel = sidebarRef.current
    if (!panel) return
    panel.isCollapsed() ? panel.expand() : panel.collapse()
  }

  return (
    <>
      <button onClick={toggleSidebar}>Toggle sidebar</button>
      <PanelGroup direction="horizontal">
        <Panel ref={sidebarRef} collapsible defaultSize={25} minSize={15}>
          <Sidebar />
        </Panel>
        <PanelResizeHandle />
        <Panel>
          <Content />
        </Panel>
      </PanelGroup>
    </>
  )
}

Persistent Sizing

// Sizes are automatically saved to localStorage under the key "main-layout"
<PanelGroup direction="horizontal" autoSaveId="main-layout">
  <Panel defaultSize={30}>...</Panel>
  <PanelResizeHandle />
  <Panel>...</Panel>
</PanelGroup>

TypeScript Tips

Use ImperativePanelHandle for the ref type:

import { type ImperativePanelHandle } from 'react-resizable-panels'

const ref = useRef<ImperativePanelHandle>(null)

// Available methods:
ref.current?.collapse()
ref.current?.expand()
ref.current?.resize(40)          // resize to 40%
ref.current?.getSize()           // get current size in %
ref.current?.isCollapsed()       // boolean

Keyboard access and accessibility

A draggable divider is useless to anyone who can't use a mouse, so the resize handles are keyboard operable out of the box. Focus a handle and the arrow keys move it in small steps, which means keyboard users and assistive technology users can adjust the layout just like everyone else. The handle also carries the right ARIA role and value attributes so a screen reader announces it as a separator with a current position.

Don't undo that for free accessibility by hiding the handle visually. Give it a focus ring and enough hit area to grab, both with a pointer and a finger. A one-pixel divider is technically draggable but practically invisible, and on touch screens it's nearly impossible to land on. Widening the handle on hover, or adding a grip icon in the middle, makes the interaction obvious without cluttering the design.

Common Gotchas

  • Container must have explicit dimensions - PanelGroup needs width and height to calculate sizes; use h-screen or set an explicit height. This is the number one reason a layout renders with zero-size panels.
  • defaultSize values must sum to 100 - across all Panel components in a group, or the library will normalize them in ways you didn't intend.
  • Resize handle styling - the handle renders a tiny element; make it visually clear (wider on hover) so users discover it.
  • Unique autoSaveId per group - if two panel groups share an id, they'll overwrite each other's saved sizes in localStorage and produce confusing results.
  • TanStack Table - a common pattern is a resizable panel layout with a navigation tree on the left and a data table on the right
  • cmdk - command palette that opens over any panel layout without disturbing the split view

Frequently Asked Questions

How do I save panel sizes between page reloads?

Add the autoSaveId prop to PanelGroup with a unique string identifier. The component will automatically save panel sizes to localStorage under that key and restore them on the next render. Make sure the id is unique per layout if you have multiple panel groups on the page.

How do I programmatically collapse and expand a panel?

Add the collapsible prop to the Panel and attach a ref typed as ImperativePanelHandle. Then call ref.current.collapse() or ref.current.expand() from your button handler. You can also check ref.current.isCollapsed() to toggle. The panel must have a minSize set for collapsible to work.

Why do my panels not render correctly without explicit container dimensions?

PanelGroup calculates panel sizes as percentages of its own dimensions. If the container has no explicit width or height (e.g. for horizontal or vertical groups), the panels may render with zero size. Wrap the PanelGroup in a container with h-screen, h-full, or a fixed pixel height.

Can I nest PanelGroup components?

Yes. Place a PanelGroup inside a Panel to create nested split layouts, like a horizontal split where one panel is itself split vertically. Each nested group manages its own panel sizes independently. Use autoSaveId on each group with different keys if you want persistent sizing for nested groups.

How do I make the resize handle more discoverable?

The PanelResizeHandle renders a minimal element by default with no visible styling. Add className to style it - typically a thin bar that widens and changes colour on hover. For example: className='w-1 bg-slate-200 hover:w-1.5 hover:bg-indigo-500 cursor-col-resize transition-all'. Consider adding an icon inside the handle for additional affordance.