TL;DR: React Resizable Panels lets users drag to resize split-view layouts. Use the
Group,Panel, andSeparatorcomponents. Supports collapsible panels, auseDefaultLayouthook for persistent sizing, and keyboard accessibility. About 11 kB min+gzip. This page documents version 4; if your code saysPanelGroup, 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
Groupcomponents - Collapsible panels -
collapsible+minSizefor panels that snap closed - Persistent sizing - the
useDefaultLayouthook saves and restores layouts throughlocalStorageor any storage object you pass - Keyboard accessible - separators support arrow keys
- Imperative API -
usePanelRef()gives youcollapse(),expand()andresize()
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 -
Groupneeds a width and a height to calculate sizes; useh-screenor set an explicit height. This is the number one reason a layout renders with zero-size panels. - Bare numbers are pixels -
defaultSize={20}andminSize={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
idper saved layout - if two groups calluseDefaultLayoutwith the same id, they'll overwrite each other's saved sizes and produce confusing results. - Old snippets won't compile -
PanelGroup,PanelResizeHandle,direction,autoSaveIdandImperativePanelHandlebelong to version 3 and earlier. Version 4 renamed them toGroup,Separator,orientation,useDefaultLayoutandPanelImperativeHandle. Our library major-version migrations article walks through the same rename.
Related
- 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