---
title: Resizable
description: "Accessible resizable panel groups and layouts with keyboard support."
sidebar:
  badge: New
---

```tsx
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

```bash
npx @madeui/cli add resizable
```

Installs `react-resizable-panels` alongside the component.

## Usage

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

## Composition

```tsx
<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

```tsx
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.

```tsx
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

```tsx
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.

```tsx
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`.

```tsx
'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](https://github.com/bvaughn/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](https://react-resizable-panels.vercel.app/) 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`.
