HeroUI Pro

Resizable

Resizable panel groups with composable handle types and variants. Built on react-resizable-panels, wired up with HeroUI design tokens.

About

The Resizable component is built on top of react-resizable-panels by bvaughn.

Usage

Wrap any number of Resizable.Panel children in a Resizable group, separated by Resizable.Handles. The group sizes each panel as a percentage of its own width (horizontal) or height (vertical).

Anatomy

import {Resizable} from "@heroui-pro/react";

export function Example() {
  return (
    <Resizable orientation="horizontal" autoSaveId="app:shell">
      <Resizable.Panel defaultSize={30} minSize={15} maxSize={45}>
        Sidebar
      </Resizable.Panel>
      <Resizable.Handle variant="primary" type="line" withIndicator />
      <Resizable.Panel>Main content</Resizable.Panel>
    </Resizable>
  );
}

Every layer is composable: Resizable.Handle renders a drag separator, and Resizable.Indicator is the inner affordance (pill or drag dots) you can also compose manually.

Vertical

Set orientation="vertical" to stack panels and resize along the Y axis.

Handle Types

Three handle affordances — use the one that matches the importance of the boundary.

  • line — minimal 1px separator. Default. Works everywhere.
  • drag — 1px separator + drag grip chip in the middle. Use when users may not realize the boundary is resizable.
  • pill — 1px separator + rounded pill grip. Highest affordance. Use sparingly, usually for critical boundaries.
  • handle — standalone pill grip with no separator line. Use when you want a drag affordance that floats between panels without a continuous divider.

Variants

Variants map directly to v3 separator tokens so the handle blends into the surface it sits on.

  • primaryseparator (lightest — default, works on the primary background)
  • secondaryseparator-secondary (for background-secondary / surface)
  • tertiaryseparator-tertiary (for darker tertiary surfaces)

Nested Groups

Panels can contain nested Resizable groups. Use different orientations to build editor-style three-pane layouts.

Collapsible Panels

Set collapsible and collapsedSize on a panel to let it collapse when dragged below its minimum size. Use handleRef for imperative collapse/expand.

With Indicator

Set withIndicator on a line-type handle to render the drag-dots affordance inline, or compose Resizable.Indicator directly for full control.

Persisted Sizes

Client-only apps

Pass autoSaveId to persist panel sizes to localStorage. Reloads restore the previous layout automatically. This is powered by react-resizable-panels and works out of the box for client-only apps (Vite, Storybook, etc.).

<Resizable autoSaveId="app:panels">
  <Resizable.Panel defaultSize={30} minSize={20}>Sidebar</Resizable.Panel>
  <Resizable.Handle />
  <Resizable.Panel defaultSize={70}>Main</Resizable.Panel>
</Resizable>

SSR-friendly persistence with cookies

On SSR frameworks, localStorage isn't available on the server, so the first paint uses the default sizes and the client re-renders after hydration. To avoid this flash, use createCookieStorage — a built-in helper that returns a cookie-backed PanelGroupStorage. Pass it to the storage prop alongside autoSaveId:

import {createCookieStorage, Resizable} from "@heroui-pro/react";

const storage = createCookieStorage();

export function ResizableLayout() {
  return (
    <Resizable autoSaveId="app:panels" storage={storage}>
      <Resizable.Panel defaultSize={30} minSize={20}>
        Sidebar
      </Resizable.Panel>
      <Resizable.Handle />
      <Resizable.Panel defaultSize={70}>Main</Resizable.Panel>
    </Resizable>
  );
}

Reads fall back to localStorage for client-only apps; writes go to both cookie and localStorage so cross-tab readers stay in sync.

Next.js App Router

On the server, read the cookie and forward the saved layout to each panel via defaultSize:

// app/layout.tsx
import {cookies} from "next/headers";

export default async function Layout({children}: {children: React.ReactNode}) {
  const store = await cookies();
  const layout = store.get("react-resizable-panels:app:panels");

  let defaultSizes = [30, 70];

  if (layout?.value) {
    try {
      const parsed = JSON.parse(layout.value);
      if (Array.isArray(parsed)) defaultSizes = parsed;
    } catch {
      // ignore malformed cookie
    }
  }

  return <ResizableLayout defaultSizes={defaultSizes}>{children}</ResizableLayout>;
}

API Reference

Resizable

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"Layout direction.
autoSaveIdstringUnique id used to persist panel sizes to localStorage.
onLayout(sizes: number[]) => voidFires on every layout change (including while dragging).
storagePanelGroupStoragelocalStorageCustom storage implementation for SSR-safe persistence.
idstringUnique identifier (required for nested groups).
handleRefRef<ImperativePanelGroupHandle>Imperative handle (setLayout, getLayout).

Resizable.Panel

PropTypeDefaultDescription
defaultSizenumberInitial panel size (percent).
minSizenumberMinimum size (percent).
maxSizenumberMaximum size (percent).
collapsiblebooleanfalseWhether the panel can collapse to collapsedSize.
collapsedSizenumberSize applied when collapsed.
idstringUnique id (required with autoSaveId + collapsible).
ordernumberPanel render order (for conditional rendering).
onCollapse() => voidFires when the panel collapses.
onExpand() => voidFires when the panel expands.
onResize(size: number) => voidFires on resize.

Resizable.Handle

PropTypeDefaultDescription
type"line" | "drag" | "pill" | "handle""line"Affordance style. handle is a standalone pill grip without the separator line.
variant"primary" | "secondary" | "tertiary""primary"Emphasis level. Maps to separator tokens.
withIndicatorbooleanSugar: render the default indicator inside the handle.
disabledbooleanfalseWhether the handle is disabled.
onDragging(isDragging: boolean) => voidFires when drag starts/ends.

Resizable.Indicator

PropTypeDefaultDescription
type"pill" | "drag""pill"Affordance style when no children are provided.
childrenReactNodeCustom content. Overrides the default affordance.

CSS Variables

The root .resizable element exposes the following variables for easy customization:

VariableDefaultDescription
--resizable-handle-size1pxVisible separator thickness.
--resizable-handle-hit-area8pxInvisible grab area around the line.
--resizable-handle-colorvar(--color-separator)Default handle color.
--resizable-handle-color-hovervar(--color-separator-secondary)Hover color.
--resizable-handle-color-activevar(--color-accent-soft)Active/dragging color.
--resizable-indicator-pill-width6pxPill indicator short axis.
--resizable-indicator-pill-height32pxPill indicator long axis.
--resizable-indicator-drag-size12pxDrag dots size.

On this page