Skip to content

Resizable Panels

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).

Layoutv0.1.0experimentalWCAG 2.2 AAView spec

Sidebar

Drag the line, or focus it and use the arrow keys.

Content

Double click the handle to restore the sizes.

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

Copies 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.json

Works, 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

Editor

Source goes here.

Console

Output goes here.

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

Files

Editor

Console

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

Navigation

Press Enter on the handle to collapse or restore this panel.

Content

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

Left

Resize, then reload the page: the sizes are remembered.

Right

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.

PropTypeDefault
childrenrequired

ResizablePanel elements with a ResizableHandle between each pair. Give the group a size: it fills its parent.

nodenone
orientation

Horizontal puts panels side by side; vertical stacks them.

"horizontal" | "vertical""horizontal"
autoSaveId

Remember the sizes in this browser under this name. Storage failures are ignored.

stringnone
onLayoutChange

Called 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[]) => voidnone
classNames

Extra 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