Skip to content
Utilities

React Resizable Panels: Utility Library Guide

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

TypeScript MIT
38.1M weekly downloads
5.4k GitHub stars
11.4 kB min+gzip bundle size
v4.12.3 latest version

Figures above measured on from the npm registry (latest version, last-week downloads) and the GitHub API (stars). Not copied from the project's own README, and re-measured rather than edited by hand, so the date tells you exactly how old these numbers are.

How the bundle size was measured. We installed react-resizable-panels@4.12.3 on its own, bundled export * from 'react-resizable-panels' with esbuild (esm, browser, minified, production), marked React external because your app already has it, and gzipped the result. Measured . This is the ceiling, not the typical cost: it imports the entire public surface with no tree-shaking credit. Pull one component from a well-shaken library and you'll pay a fraction of it. We publish the ceiling because it's the number we can reproduce, and reproducing it is the point. For React Resizable Panels, a typical import -- just { Group } from react-resizable-panels, bundled and gzipped the same way -- measures 10.8 kB min+gzip, against the 11.4 kB min+gzip ceiling above.

panelslayoutresizablesplit-viewdrag

The real, running example behind our measurements of this package -- not a screenshot.

Maintenance Health

Who keeps React Resizable Panels alive, and how it behaves when something breaks -- measured from the npm registry and the package's own repository, not the project's README.

Who maintains this
1 npm publisher • One maintainer
Last npm release
Last repo activity
Open issues
11
React peer range
^18.0.0 || ^19.0.0

TL;DR: React Resizable Panels lets users drag to resize split-view layouts. Use the Group, Panel, and Separator components. Supports collapsible panels, a useDefaultLayout hook for persistent sizing, and keyboard accessibility. About 11 kB min+gzip. This page documents version 4; if your code says PanelGroup, it was written for version 3 or earlier. Official docs: react-resizable-panels.vercel.app.

What is React Resizable Panels?

React Resizable Panels provides <Group>, <Panel>, and <Separator> 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 Group sets an orientation, either horizontal or vertical. Inside it, Panel elements hold your content, and Separator elements sit between them as draggable dividers. Sizes can be percentages of the group, which is what keeps layouts fluid when the window resizes, or fixed units. The library does the math so the panels always add up to the full space, even mid-drag.

Why do percentages matter? Because pixel-only 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. Version 4 also takes pixels, rem and viewport units for the same props, so you can pin a sidebar's minimum width in pixels and let the rest flex.

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 - orientation="horizontal" or "vertical"
  • Nested groups - panels can contain other Group components
  • Collapsible panels - collapsible + minSize for panels that snap closed
  • Persistent sizing - the useDefaultLayout hook saves and restores layouts through localStorage or any storage object you pass
  • Keyboard accessible - separators support arrow keys
  • Imperative API - usePanelRef() gives you collapse(), expand() and resize()

Installation

npm install react-resizable-panels

Basic Usage

import { Group, Panel, Separator } from 'react-resizable-panels'

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

      <Separator 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>

      <Separator 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>
    </Group>
  )
}

Mind the units. In version 4 a bare number is read as pixels and a string without a unit as a percentage, so defaultSize={20} is a 20-pixel sidebar, not 20 percent. Writing the unit out, as above, removes the guesswork.

Collapsible Panel with Toggle Button

import { Group, Panel, Separator, usePanelRef } from 'react-resizable-panels'

function CollapsibleSidebar() {
  const sidebarRef = usePanelRef()

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

  return (
    <>
      <button onClick={toggleSidebar}>Toggle sidebar</button>
      <Group orientation="horizontal">
        <Panel panelRef={sidebarRef} collapsible defaultSize="25%" minSize="15%">
          <Sidebar />
        </Panel>
        <Separator />
        <Panel>
          <Content />
        </Panel>
      </Group>
    </>
  )
}

The ref goes in a prop called panelRef. A plain ref is not the handle any more.

Persistent Sizing

import { Group, Panel, Separator, useDefaultLayout } from 'react-resizable-panels'

function PersistentLayout() {
  // The id decides which saved layout this group reads and writes
  const { defaultLayout, onLayoutChanged } = useDefaultLayout({ id: 'main-layout' })

  return (
    <Group
      orientation="horizontal"
      defaultLayout={defaultLayout}
      onLayoutChanged={onLayoutChanged}
    >
      <Panel defaultSize="30%">...</Panel>
      <Separator />
      <Panel>...</Panel>
    </Group>
  )
}

The hook accepts a storage option for sessionStorage, cookies or your own store, and onlySaveAfterUserInteractions to ignore layout changes caused by window resizes. Use onLayoutChanged, not onLayoutChange: the second fires on every pointer move during a drag, while the first waits until the pointer is released.

TypeScript Tips

usePanelRef() returns a ref already typed as PanelImperativeHandle, and useGroupRef() does the same for the group:

import { usePanelRef } from 'react-resizable-panels'

const ref = usePanelRef()

// Available methods:
ref.current?.collapse()
ref.current?.expand()
ref.current?.resize('40%')       // a bare number would mean 40 pixels
ref.current?.getSize()           // { asPercentage, inPixels }
ref.current?.isCollapsed()       // boolean

Keyboard access and accessibility

A draggable divider is useless to anyone who can't use a mouse, so the separators are keyboard operable out of the box. Focus a separator 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 component is named after the ARIA separator role it renders, so a screen reader announces it as a divider rather than as an anonymous div.

Don't undo that for free accessibility by hiding the separator 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 - Group needs a width and a 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.
  • Bare numbers are pixels - defaultSize={20} and minSize={120} mean pixels. Write "20%" when you mean a percentage.
  • Separator styling - the separator renders a tiny element; make it visually clear (wider on hover) so users discover it.
  • Unique id per saved layout - if two groups call useDefaultLayout with the same id, they'll overwrite each other's saved sizes and produce confusing results.
  • Old snippets won't compile - PanelGroup, PanelResizeHandle, direction, autoSaveId and ImperativePanelHandle belong to version 3 and earlier. Version 4 renamed them to Group, Separator, orientation, useDefaultLayout and PanelImperativeHandle. Our library major-version migrations article walks through the same rename.
  • 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?

Version 4 dropped the autoSaveId prop. Call the useDefaultLayout hook with a unique id, pass the defaultLayout it returns to Group, and attach its onLayoutChanged callback to the same Group. The hook reads and writes through a storage object (localStorage, sessionStorage or your own), so the sizes come back on the next load. Give every layout its own id, or two groups will overwrite each other.

How do I programmatically collapse and expand a panel?

Add the collapsible prop to the Panel and pass it a ref from the usePanelRef hook, which is typed as PanelImperativeHandle. Then call ref.current.collapse() or ref.current.expand() from your button handler, and check ref.current.isCollapsed() to decide which one. A collapsible panel snaps shut when it is dragged below its minSize, so set one.

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

Group divides its own box between the panels. If the container has no explicit width or height, the panels may render with zero size. Give the Group a class such as h-screen or h-full, or wrap it in a container with a fixed pixel height.

Can I nest Group components?

Yes. Place a Group 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. Call useDefaultLayout once per group, with a different id each time, if you want every nested group to remember its sizes.

How do I make the separator more discoverable?

The Separator 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.