27.7k

Resizable

Resizable panel groups with composable handle types and variants

About

The Resizable component is built on top of react-resizable-panels by bvaughn, wired up with DarkCode UI design tokens and the compound-component pattern.

Import

import { Resizable } from '@darkcode-ui/react';

Anatomy

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

<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 the drag separator, and Resizable.Indicator is the inner affordance (pill or drag dots) you can also compose manually.

Usage

import {Resizable} from "@darkcode-ui/react";

export function ResizableBasic() {
  return (
    <div className="h-72 w-full overflow-hidden rounded-xl border border-border bg-background">

Vertical

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

import {Resizable} from "@darkcode-ui/react";

export function ResizableVertical() {
  return (
    <div className="h-96 w-full overflow-hidden rounded-xl border border-border bg-background">

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.
import {Resizable} from "@darkcode-ui/react";

const types = ["line", "drag", "pill", "handle"] as const;

export function ResizableHandleTypes() {

Variants

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

  • primary → separator (lightest — default, works on the primary background)
  • secondary → separator-secondary (for background-secondary / surface)
  • tertiary → separator-tertiary (for darker tertiary surfaces)
import {Resizable} from "@darkcode-ui/react";

const variants = ["primary", "secondary", "tertiary"] as const;

export function ResizableVariants() {

Nested Groups

Panels can contain nested Resizable groups. Use different orientations to build editor-style three-pane layouts. Give each nested group a unique id.

import {Resizable} from "@darkcode-ui/react";

export function ResizableNested() {
  return (
    <div className="h-96 w-full overflow-hidden rounded-xl border border-border bg-background">

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.

"use client";

import type {ImperativePanelHandle} from "@darkcode-ui/react";

import {Button, Resizable} from "@darkcode-ui/react";

With Indicator

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

import {Resizable} from "@darkcode-ui/react";

export function ResizableWithIndicator() {
  return (
    <div className="h-72 w-full overflow-hidden rounded-xl border border-border bg-background">

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 '@darkcode-ui/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 prefer the cookie (so the server can forward saved layouts) and fall back to localStorage for client-only apps; writes go to both 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>;
}

Styling

Passing Tailwind CSS classes

import {Resizable} from '@darkcode-ui/react';

function CustomResizable() {
  return (
    <Resizable className="rounded-xl border border-border" orientation="horizontal">
      <Resizable.Panel className="bg-default-soft" defaultSize={30}>
        Sidebar
      </Resizable.Panel>
      <Resizable.Handle className="data-[type=line]:[--resizable-handle-color:var(--color-accent)]" />
      <Resizable.Panel>Main</Resizable.Panel>
    </Resizable>
  );
}

Customizing the component classes

To customize the Resizable component classes, you can use the @layer components directive.

@layer components {
  .resizable__handle {
    --resizable-handle-hit-area: 12px;
  }

  .resizable__indicator--pill {
    @apply bg-accent-soft;
  }
}

CSS Classes

The Resizable component uses these CSS classes:

Base Classes

  • .resizable - Root panel group
  • .resizable--horizontal / .resizable--vertical - Orientation modifiers
  • .resizable__panel - Individual panel

Handle Classes

  • .resizable__handle - Resize handle (separator)
  • .resizable__handle--line / --drag / --pill / --handle - Handle type modifiers
  • .resizable__handle--primary / --secondary / --tertiary - Emphasis modifiers

Indicator Classes

  • .resizable__indicator - Grip affordance
  • .resizable__indicator--pill - Rounded pill grip
  • .resizable__indicator--drag - Drag-dots grip

Data Attributes

  • [data-orientation] on the root — horizontal or vertical
  • [data-resize-handle-state] on the handle — inactive, hover, or drag
  • [data-disabled="true"] on a disabled handle

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.

API Reference

Resizable

The root panel group. Wraps react-resizable-panels' PanelGroup.

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

Resizable.Panel

PropTypeDefaultDescription
defaultSizenumber—Initial panel size (percent).
minSizenumber—Minimum size (percent).
maxSizenumber—Maximum size (percent).
collapsiblebooleanfalseWhether the panel can collapse to collapsedSize.
collapsedSizenumber—Size applied when collapsed.
idstring—Unique id (required with autoSaveId + collapsible).
ordernumber—Panel render order (for conditional rendering).
handleRefRef<ImperativePanelHandle>—Imperative handle (collapse, expand, resize).
onCollapse() => void—Fires when the panel collapses.
onExpand() => void—Fires when the panel expands.
onResize(size: number) => void—Fires 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.
withIndicatorboolean—Sugar: render the default indicator inside the handle.
disabledbooleanfalseWhether the handle is disabled.
onDragging(isDragging: boolean) => void—Fires when drag starts/ends.

Resizable.Indicator

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

createCookieStorage

createCookieStorage(options?) returns a cookie-backed PanelGroupStorage.

OptionTypeDefaultDescription
maxAgenumber31536000Cookie lifetime in seconds.
pathstring"/"Cookie path.
sameSite"Lax" | "Strict" | "None""Lax"SameSite policy.

On this page