Skip to content
Esc
navigateopen⌘Jpreview
On this page

Resizable

Accessible resizable panel groups and layouts with keyboard support.

One
Two
import * as stylex from '@stylexjs/stylex';

import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from '@/components/ui/resizable';
import { container, fontSize, fontWeight, space, stroke } from '@/lib/constants.stylex';
import { colors, font, radius } from '@/lib/tokens.stylex';

export default function ResizableDemo() {
  return (
    <div {...stylex.props(styles.frame)}>
      <ResizablePanelGroup>
        <ResizablePanel defaultSize="50%" minSize="20%">
          <div {...stylex.props(styles.content)}>
            <span {...stylex.props(styles.label)}>One</span>
          </div>
        </ResizablePanel>
        <ResizableHandle />
        <ResizablePanel defaultSize="50%" minSize="20%">
          <div {...stylex.props(styles.content)}>
            <span {...stylex.props(styles.label)}>Two</span>
          </div>
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  );
}

const styles = stylex.create({
  // The group fills its parent, so the frame sets the size and the edge.
  frame: {
    borderColor: colors.border,
    borderRadius: radius.lg,
    borderStyle: 'solid',
    borderWidth: stroke.border,
    height: container.xs,
    maxWidth: container.xl,
    overflow: 'hidden',
    width: '100%',
  },
  content: {
    alignItems: 'center',
    backgroundColor: colors.background,
    color: colors.foreground,
    display: 'flex',
    fontFamily: font.sans,
    fontSize: fontSize.sm,
    fontWeight: fontWeight.semibold,
    height: '100%',
    justifyContent: 'center',
    padding: space.s6,
  },
  // Panels get narrow as the handle moves; a one-word label truncates
  // instead of spilling across the divider.
  label: {
    maxWidth: '100%',
    overflow: 'hidden',
    textOverflow: 'ellipsis',
    whiteSpace: 'nowrap',
  },
});

Install

npx @madeui/cli add resizable

Installs react-resizable-panels alongside the component.

Usage

import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from '@/components/ui/resizable';

Composition

<ResizablePanelGroup>
  <ResizablePanel />
  <ResizableHandle />
  <ResizablePanel />
</ResizablePanelGroup>

The group fills its parent (width and height 100%), so size the wrapper. Sizes are percentages when passed as strings ("30%") and pixels when passed as numbers (200); other CSS units work as strings too ("12rem", "50vh").

Keyboard

The handle is focusable. Arrow keys move it in 5% steps along the group’s axis, Home and End jump to the neighbouring panel’s minimum and maximum, Enter collapses or expands a collapsible panel, and F6 cycles between the handles of a group.

Vertical

Header
Content
import * as stylex from '@stylexjs/stylex';

import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from '@/components/ui/resizable';
import { container, fontSize, fontWeight, space, stroke } from '@/lib/constants.stylex';
import { colors, font, radius } from '@/lib/tokens.stylex';

export default function ResizableVertical() {
  return (
    <div {...stylex.props(styles.frame)}>
      <ResizablePanelGroup orientation="vertical">
        <ResizablePanel defaultSize="30%" minSize="20%">
          <div {...stylex.props(styles.content)}>
            <span {...stylex.props(styles.label)}>Header</span>
          </div>
        </ResizablePanel>
        <ResizableHandle />
        <ResizablePanel defaultSize="70%" minSize="20%">
          <div {...stylex.props(styles.content)}>
            <span {...stylex.props(styles.label)}>Content</span>
          </div>
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  );
}

const styles = stylex.create({
  frame: {
    borderColor: colors.border,
    borderRadius: radius.lg,
    borderStyle: 'solid',
    borderWidth: stroke.border,
    height: container.card,
    maxWidth: container.lg,
    overflow: 'hidden',
    width: '100%',
  },
  content: {
    alignItems: 'center',
    backgroundColor: colors.background,
    color: colors.foreground,
    display: 'flex',
    fontFamily: font.sans,
    fontSize: fontSize.sm,
    fontWeight: fontWeight.semibold,
    height: '100%',
    justifyContent: 'center',
    padding: space.s6,
  },
  // Panels get narrow as the handle moves; a one-word label truncates
  // instead of spilling across the divider.
  label: {
    maxWidth: '100%',
    overflow: 'hidden',
    textOverflow: 'ellipsis',
    whiteSpace: 'nowrap',
  },
});

Handle

withHandle renders a grip on the line.

Sidebar
Content
import * as stylex from '@stylexjs/stylex';

import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from '@/components/ui/resizable';
import { container, fontSize, fontWeight, space, stroke } from '@/lib/constants.stylex';
import { colors, font, radius } from '@/lib/tokens.stylex';

export default function ResizableWithHandle() {
  return (
    <div {...stylex.props(styles.frame)}>
      <ResizablePanelGroup>
        <ResizablePanel defaultSize="25%" minSize="15%">
          <div {...stylex.props(styles.content)}>
            <span {...stylex.props(styles.label)}>Sidebar</span>
          </div>
        </ResizablePanel>
        <ResizableHandle withHandle />
        <ResizablePanel defaultSize="75%" minSize="30%">
          <div {...stylex.props(styles.content)}>
            <span {...stylex.props(styles.label)}>Content</span>
          </div>
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  );
}

const styles = stylex.create({
  frame: {
    borderColor: colors.border,
    borderRadius: radius.lg,
    borderStyle: 'solid',
    borderWidth: stroke.border,
    height: container.xs,
    maxWidth: container.xl,
    overflow: 'hidden',
    width: '100%',
  },
  content: {
    alignItems: 'center',
    backgroundColor: colors.background,
    color: colors.foreground,
    display: 'flex',
    fontFamily: font.sans,
    fontSize: fontSize.sm,
    fontWeight: fontWeight.semibold,
    height: '100%',
    justifyContent: 'center',
    padding: space.s6,
  },
  // Panels get narrow as the handle moves; a one-word label truncates
  // instead of spilling across the divider.
  label: {
    maxWidth: '100%',
    overflow: 'hidden',
    textOverflow: 'ellipsis',
    whiteSpace: 'nowrap',
  },
});

Nested

One
Two
Three
import * as stylex from '@stylexjs/stylex';

import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from '@/components/ui/resizable';
import { container, fontSize, fontWeight, space, stroke } from '@/lib/constants.stylex';
import { colors, font, radius } from '@/lib/tokens.stylex';

export default function ResizableNested() {
  return (
    <div {...stylex.props(styles.frame)}>
      <ResizablePanelGroup>
        <ResizablePanel defaultSize="50%" minSize="20%">
          <div {...stylex.props(styles.content)}>
            <span {...stylex.props(styles.label)}>One</span>
          </div>
        </ResizablePanel>
        <ResizableHandle withHandle />
        <ResizablePanel defaultSize="50%" minSize="20%">
          <ResizablePanelGroup orientation="vertical">
            <ResizablePanel defaultSize="25%" minSize="20%">
              <div {...stylex.props(styles.content)}>
                <span {...stylex.props(styles.label)}>Two</span>
              </div>
            </ResizablePanel>
            <ResizableHandle withHandle />
            <ResizablePanel defaultSize="75%" minSize="20%">
              <div {...stylex.props(styles.content)}>
                <span {...stylex.props(styles.label)}>Three</span>
              </div>
            </ResizablePanel>
          </ResizablePanelGroup>
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  );
}

const styles = stylex.create({
  frame: {
    borderColor: colors.border,
    borderRadius: radius.lg,
    borderStyle: 'solid',
    borderWidth: stroke.border,
    height: container.card,
    maxWidth: container.xl,
    overflow: 'hidden',
    width: '100%',
  },
  content: {
    alignItems: 'center',
    backgroundColor: colors.background,
    color: colors.foreground,
    display: 'flex',
    fontFamily: font.sans,
    fontSize: fontSize.sm,
    fontWeight: fontWeight.semibold,
    height: '100%',
    justifyContent: 'center',
    padding: space.s6,
  },
  // Panels get narrow as the handle moves; a one-word label truncates
  // instead of spilling across the divider.
  label: {
    maxWidth: '100%',
    overflow: 'hidden',
    textOverflow: 'ellipsis',
    whiteSpace: 'nowrap',
  },
});

Collapsible

A collapsible panel snaps to collapsedSize when dragged below its minSize. Double-clicking a handle restores the default sizes.

Sidebar
Drag the handle left to collapse
import * as stylex from '@stylexjs/stylex';

import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from '@/components/ui/resizable';
import { container, fontSize, fontWeight, space, stroke } from '@/lib/constants.stylex';
import { colors, font, radius } from '@/lib/tokens.stylex';

export default function ResizableCollapsible() {
  return (
    <div {...stylex.props(styles.frame)}>
      <ResizablePanelGroup>
        <ResizablePanel
          collapsible
          collapsedSize="8%"
          defaultSize="30%"
          minSize="20%"
        >
          <div {...stylex.props(styles.content)}>
            <span {...stylex.props(styles.label)}>Sidebar</span>
          </div>
        </ResizablePanel>
        <ResizableHandle withHandle />
        <ResizablePanel defaultSize="70%" minSize="40%">
          <div {...stylex.props(styles.content)}>Drag the handle left to collapse</div>
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  );
}

const styles = stylex.create({
  frame: {
    borderColor: colors.border,
    borderRadius: radius.lg,
    borderStyle: 'solid',
    borderWidth: stroke.border,
    height: container.xs,
    maxWidth: container.xl,
    overflow: 'hidden',
    width: '100%',
  },
  content: {
    alignItems: 'center',
    backgroundColor: colors.background,
    color: colors.foreground,
    display: 'flex',
    fontFamily: font.sans,
    fontSize: fontSize.sm,
    fontWeight: fontWeight.semibold,
    height: '100%',
    justifyContent: 'center',
    padding: space.s6,
    textAlign: 'center',
  },
  // Panels get narrow as the handle moves; a one-word label truncates
  // instead of spilling across the divider.
  label: {
    maxWidth: '100%',
    overflow: 'hidden',
    textOverflow: 'ellipsis',
    whiteSpace: 'nowrap',
  },
});

Persisted layout

useDefaultLayout saves the layout under an id and restores it on the next visit. Give each panel a stable id.

'use client';

import * as React from 'react';

import * as stylex from '@stylexjs/stylex';

import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
  useDefaultLayout,
} from '@/components/ui/resizable';
import { container, fontSize, fontWeight, space, stroke } from '@/lib/constants.stylex';
import { colors, font, radius } from '@/lib/tokens.stylex';

// The saved layout lives in localStorage, which only exists in the browser.
// Mount the group after hydration so the server markup and the first client
// render match; the frame keeps its size meanwhile.
const subscribe = () => () => {};
const useMounted = () =>
  React.useSyncExternalStore(
    subscribe,
    () => true,
    () => false
  );

export default function ResizablePersisted() {
  const mounted = useMounted();
  return (
    <div {...stylex.props(styles.frame)}>{mounted ? <PersistedGroup /> : null}</div>
  );
}

function PersistedGroup() {
  const { defaultLayout, onLayoutChanged } = useDefaultLayout({
    id: 'resizable-persisted-example',
  });

  return (
    <ResizablePanelGroup
      defaultLayout={defaultLayout}
      onLayoutChanged={onLayoutChanged}
    >
      <ResizablePanel id="left" defaultSize="50%" minSize="20%">
        <div {...stylex.props(styles.content)}>Resize me</div>
      </ResizablePanel>
      <ResizableHandle withHandle />
      <ResizablePanel id="right" defaultSize="50%" minSize="20%">
        <div {...stylex.props(styles.content)}>Then reload the page</div>
      </ResizablePanel>
    </ResizablePanelGroup>
  );
}

const styles = stylex.create({
  frame: {
    borderColor: colors.border,
    borderRadius: radius.lg,
    borderStyle: 'solid',
    borderWidth: stroke.border,
    height: container.xs,
    maxWidth: container.xl,
    overflow: 'hidden',
    width: '100%',
  },
  content: {
    alignItems: 'center',
    backgroundColor: colors.background,
    color: colors.foreground,
    display: 'flex',
    fontFamily: font.sans,
    fontSize: fontSize.sm,
    fontWeight: fontWeight.semibold,
    height: '100%',
    justifyContent: 'center',
    padding: space.s6,
    textAlign: 'center',
  },
});

API reference

Built on react-resizable-panels v4: ResizablePanelGroup wraps Group, ResizablePanel wraps Panel, and ResizableHandle wraps Separator. The tables below cover the props this library adds or changes — every other prop (defaultSize, minSize, maxSize, collapsible, onLayoutChanged, panelRef, …) is forwarded to the underlying part; see the react-resizable-panels docs for the full list. useDefaultLayout is re-exported unchanged.

ResizablePanelGroup

Prop Type Default Description
orientation 'horizontal' | 'vertical' 'horizontal'
resizeTargetMinimumSize { coarse: number; fine: number } { coarse: 24, fine: 10 } Size in CSS pixels of the pointer target around each handle. The handle draws a 1px line; this is what the pointer actually has to hit. coarse is 24 so a touch drag meets the WCAG 2.2 minimum target size.
style StyleXStyles StyleX styles merged last — always win over the component’s own styles. The library sets width, height, display, flex-direction, and overflow inline.

ResizableHandle

Prop Type Default Description
withHandle boolean false Render a centered grip pill on the line.
aria-label string 'Resize panels' Accessible name for the handle, which is focusable and announced as a splitter. Override it when a group has several handles that need telling apart.
style StyleXStyles StyleX styles merged last — always win over the component’s own styles.

Styling

ResizablePanelGroup, ResizablePanel, ResizableHandle accept style (StyleXStyles, merged last so caller overrides always win). ResizablePanel styles land on the panel’s content box, which the library gives overflow: auto.

Was this page helpful?