Panels side by side or stacked that the person can resize by dragging or with the keyboard, for a sidebar and content, an editor and a console, or a split view. Build it from ResizablePanelGroup, ResizablePanel (id, defaultSize, minSize, maxSize, collapsible, collapsedSize) and ResizableHandle (withHandle, label).
import type { ReactNode } from "react";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@rdloom/react";
function Pane({ title, children }: { title: string; children?: ReactNode }) {
return (
<div className="flex h-full flex-col gap-1 p-4 text-sm text-[var(--rd-color-text-default)]">
<p className="font-semibold">{title}</p>
{children ? <p className="text-[var(--rd-color-text-muted)]">{children}</p> : null}
</div>
);
}
export default function ResizablePanelsHorizontalExample() {
return (
<div className="mx-auto h-72 w-[40rem] max-w-full overflow-hidden rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)]">
<ResizablePanelGroup>
<ResizablePanel defaultSize={40} minSize={20} maxSize={55}>
<Pane title="Sidebar">Drag the line, or focus it and use the arrow keys.</Pane>
</ResizablePanel>
<ResizableHandle withHandle label="Resize sidebar" />
<ResizablePanel>
<Pane title="Content">Double click the handle to restore the sizes.</Pane>
</ResizablePanel>
</ResizablePanelGroup>
</div>
);
}Installation
npx rdloom add resizable-panelsCopies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes.
Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/resizable-panels.jsonWorks, but without upgrade tracking.
Usage
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@rdloom/react";
<div className="flex h-full flex-col gap-1 p-4 text-sm text-[var(--rd-color-text-default)]">
<p className="font-semibold">{title}</p>
{children ? <p className="text-[var(--rd-color-text-muted)]">{children}</p> : null}
</div>
);
}
export default function ResizablePanelsHorizontalExample() {
return (
<div className="mx-auto h-72 w-[40rem] max-w-full overflow-hidden rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)]">
<ResizablePanelGroup>
<ResizablePanel defaultSize={40} minSize={20} maxSize={55}>
<Pane title="Sidebar">Drag the line, or focus it and use the arrow keys.</Pane>
</ResizablePanel>
<ResizableHandle withHandle label="Resize sidebar" />
<ResizablePanel>
<Pane title="Content">Double click the handle to restore the sizes.</Pane>
</ResizablePanel>
</ResizablePanelGroup>
</div>Vertical
import type { ReactNode } from "react";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@rdloom/react";
function Pane({ title, children }: { title: string; children?: ReactNode }) {
return (
<div className="flex h-full flex-col gap-1 p-4 text-sm text-[var(--rd-color-text-default)]">
<p className="font-semibold">{title}</p>
{children ? <p className="text-[var(--rd-color-text-muted)]">{children}</p> : null}
</div>
);
}
export default function ResizablePanelsVerticalExample() {
return (
<div className="mx-auto h-72 w-[40rem] max-w-full overflow-hidden rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)]">
<ResizablePanelGroup orientation="vertical">
<ResizablePanel defaultSize={65} minSize={20}>
<Pane title="Editor">Source goes here.</Pane>
</ResizablePanel>
<ResizableHandle withHandle label="Resize console" />
<ResizablePanel minSize={15}>
<Pane title="Console">Output goes here.</Pane>
</ResizablePanel>
</ResizablePanelGroup>
</div>
);
}Nested
import type { ReactNode } from "react";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@rdloom/react";
function Pane({ title, children }: { title: string; children?: ReactNode }) {
return (
<div className="flex h-full flex-col gap-1 p-4 text-sm text-[var(--rd-color-text-default)]">
<p className="font-semibold">{title}</p>
{children ? <p className="text-[var(--rd-color-text-muted)]">{children}</p> : null}
</div>
);
}
export default function ResizablePanelsNestedExample() {
return (
<div className="mx-auto h-72 w-[40rem] max-w-full overflow-hidden rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)]">
<ResizablePanelGroup>
<ResizablePanel defaultSize={30} minSize={20}>
<Pane title="Files" />
</ResizablePanel>
<ResizableHandle withHandle label="Resize files" />
<ResizablePanel>
<ResizablePanelGroup orientation="vertical">
<ResizablePanel defaultSize={60} minSize={20}>
<Pane title="Editor" />
</ResizablePanel>
<ResizableHandle withHandle label="Resize console" />
<ResizablePanel minSize={15}>
<Pane title="Console" />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
</div>
);
}Collapsible
import type { ReactNode } from "react";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@rdloom/react";
function Pane({ title, children }: { title: string; children?: ReactNode }) {
return (
<div className="flex h-full flex-col gap-1 p-4 text-sm text-[var(--rd-color-text-default)]">
<p className="font-semibold">{title}</p>
{children ? <p className="text-[var(--rd-color-text-muted)]">{children}</p> : null}
</div>
);
}
export default function ResizablePanelsCollapsibleExample() {
return (
<div className="mx-auto h-72 w-[40rem] max-w-full overflow-hidden rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)]">
<ResizablePanelGroup>
<ResizablePanel defaultSize={40} minSize={25} maxSize={55} collapsible>
<Pane title="Navigation">Press Enter on the handle to collapse or restore this panel.</Pane>
</ResizablePanel>
<ResizableHandle withHandle label="Resize navigation" />
<ResizablePanel>
<Pane title="Content" />
</ResizablePanel>
</ResizablePanelGroup>
</div>
);
}Persisted
Last change: none yet
import type { ReactNode } from "react";
import { useState } from "react";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@rdloom/react";
function Pane({ title, children }: { title: string; children?: ReactNode }) {
return (
<div className="flex h-full flex-col gap-1 p-4 text-sm text-[var(--rd-color-text-default)]">
<p className="font-semibold">{title}</p>
{children ? <p className="text-[var(--rd-color-text-muted)]">{children}</p> : null}
</div>
);
}
export default function ResizablePanelsPersistedExample() {
const [layout, setLayout] = useState<number[]>([]);
return (
<div className="mx-auto flex w-[40rem] max-w-full flex-col gap-2">
<div className="mx-auto h-72 w-[40rem] max-w-full overflow-hidden rounded-[var(--rd-radius-overlay)] border border-[var(--rd-color-border-default)]">
<ResizablePanelGroup autoSaveId="docs-persisted-example" onLayoutChange={(sizes) => setLayout(sizes.map((s) => Math.round(s)))}>
<ResizablePanel defaultSize={40} minSize={20}>
<Pane title="Left">Resize, then reload the page: the sizes are remembered.</Pane>
</ResizablePanel>
<ResizableHandle withHandle label="Resize panels" />
<ResizablePanel minSize={20}>
<Pane title="Right" />
</ResizablePanel>
</ResizablePanelGroup>
</div>
<p className="text-center text-sm text-[var(--rd-color-text-muted)]">Last change: {layout.length ? layout.join(" / ") : "none yet"}</p>
</div>
);
}API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
childrenrequiredResizablePanel elements with a ResizableHandle between each pair. Give the group a size: it fills its parent. | node | none |
orientationHorizontal puts panels side by side; vertical stacks them. | "horizontal" | "vertical" | "horizontal" |
autoSaveIdRemember the sizes in this browser under this name. Storage failures are ignored. | string | none |
onLayoutChangeCalled with every panel's size in percent (summing to 100) whenever the layout changes. During a drag it is called as the pointer moves. | (sizes: number[]) => void | none |
classNamesExtra class names for single parts. Keys: root, panel, handle, grip. | Partial<Record<"root" | "panel" | "handle" | "grip", string>> | none |
Accessibility
Role separator, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.
Keyboard
- Tab moves to each handle
- Arrow keys along the layout resize by 1 percent; with Shift by 10 percent
- Home and End move the handle to the smallest and largest size of the panel before it
- Enter collapses or restores a collapsible neighbouring panel
Screen readers announce
- The handle is announced as a separator with its name and the size of the panel before it in percent
- Collapsing a panel changes the announced value to its collapsed size
What your code must do
- Each handle has role=separator, aria-orientation, aria-valuenow, aria-valuemin and aria-valuemax of the panel before it, and aria-controls pointing at that panel
- Each handle has an accessible name
- A collapsed panel with no visible size is inert
- Pointer drag uses pointer capture and works with touch; double click restores the default sizes
- Collapse animation is off under prefers-reduced-motion
- The visible grip has a hit area larger than its line
Guidelines
Use it when
- Two or three regions where people want to choose the split: navigation and content, code and output
- Layouts that stay on one screen on desktop
Avoid it when
- Phone layouts: stack the content instead
- Fixed layouts where the split should never change
Don't
- Panels without a minimum size, so content can be dragged to nothing
- Hiding the only way to a task in a collapsed panel
Design tokens
The semantic tokens this component uses. Change them once and every component follows; see Design tokens.
--rd-color-border-default--rd-color-border-strong--rd-color-action-primary--rd-color-surface-raised--rd-color-text-muted--rd-color-focus-ring--rd-radius-control