Resizable

Resizable panel groups with draggable, keyboard-friendly handles, collapsible panels and layouts that survive a reload.

Preview

Usage

import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from '@vitnode/core/components/ui/resizable'
<div className="h-80">
  <ResizablePanelGroup className="rounded-lg border">
    <ResizablePanel defaultSize="35%">Sidebar</ResizablePanel>
    <ResizableHandle />
    <ResizablePanel>Content</ResizablePanel>
  </ResizablePanelGroup>
</div>

The group sets an inline height: 100%, so a height class on it is ignored. Give the parent a height instead.

Groups nest: put a ResizablePanelGroup inside a ResizablePanel to split it again, like the terminal in the preview.

Vertical

<ResizablePanelGroup orientation="vertical">

With handle

withHandle adds a grip, so people know the divider can be grabbed.

<ResizableHandle withHandle />

Sizes

The value's type decides the unit:

  • a string without a unit is a percentage: "30" equals "30%";
  • a number is pixels: minSize={180} is 180px, not 180%;
  • other CSS units work too: "12rem", "20vw".
<ResizablePanel defaultSize="25%" maxSize="40%" minSize={180}>

Panels without defaultSize share the leftover space. Double-click a handle to reset its panel to defaultSize.

Collapsible

A collapsible panel snaps to collapsedSize once dragged below minSize. For a toggle button, call collapse() or expand() on its panelRef.

<ResizablePanel collapsedSize="8%" collapsible defaultSize="35%" minSize="25%">

Persisting the layout

defaultLayout takes a map of panel id to percentage. onLayoutChanged fires after every finished resize. Wire both to storage, and give each panel a stable id.

const storageKey = 'my-layout'

const readLayout = () => {
  try {
    return localStorage.getItem(storageKey)
  } catch {
    return null
  }
}

export const RememberedLayout = () => {
  const stored = React.useSyncExternalStore(
    () => () => {},
    readLayout,
    () => null,
  )

  return (
    <ResizablePanelGroup
      defaultLayout={stored ? JSON.parse(stored) : undefined}
      onLayoutChanged={(layout, meta) => {
        if (!meta.isUserInteraction) return

        localStorage.setItem(
          storageKey,
          JSON.stringify(meta.requestedLayout ?? layout),
        )
      }}
    >
      <ResizablePanel defaultSize="40%" id="sidebar">
        Sidebar
      </ResizablePanel>
      <ResizableHandle withHandle />
      <ResizablePanel id="content">Content</ResizablePanel>
    </ResizablePanelGroup>
  )
}

The null server snapshot keeps SSR happy: the server renders default sizes, then the browser swaps in the saved ones. Save meta.requestedLayout when it exists. It's what the user asked for, before a small screen squeezed it.

Accessibility

  • Each handle is a focusable role="separator" with aria-valuenow, aria-valuemin, aria-valuemax and aria-controls.
  • Arrow keys resize by 5% along the group's orientation.
  • Home and End jump to the minimum or maximum size.
  • Enter collapses or expands a collapsible panel.
  • F6 moves to the next handle, Shift + F6 to the previous one.
  • Touch dragging only claims the resize axis, so panels still scroll the other way.

Props

ResizablePanelGroup

Prop

Type

ResizablePanel

Prop

Type

ResizableHandle

Prop

Type

API Reference

react-resizable-panels