---
title: Resizable
description: Panels that share space and resize with a draggable, keyboard-accessible handle.
---

<PackageInfo name="resizable" />

**bun**

```bash
bun add @data-slot/resizable
```

**npm**

```bash
npm install @data-slot/resizable
```

**pnpm**

```bash
pnpm add @data-slot/resizable
```

## Anatomy

Place one handle between each pair of panels. Sizes are percentages of the space shared by the panels; the component applies the flex layout, while you style the panels and handles.

```html
<div data-slot="resizable" data-direction="horizontal">
  <div data-slot="resizable-panel" data-default-size="50" data-min-size="20">
    Panel A
  </div>
  <div data-slot="resizable-handle" aria-label="Resize panels"></div>
  <div data-slot="resizable-panel" data-default-size="50" data-min-size="20">
    Panel B
  </div>
</div>
```

Use `data-default-size` for the initial split and `data-min-size` / `data-max-size` for limits. Panels without a default size share the remaining space. Give each handle an accessible name with `aria-label` or `aria-labelledby`.

## Examples

### Resize side by side

Drag the handle, or focus it with `Tab` and use the left and right arrow keys. Each panel keeps at least 20% of the available space. `Home` and `End` move to the limits.

<Example name="resizable" />

### Stack panels vertically

Set `data-direction="vertical"` and give the group a height. The handle moves up and down, and the up and down arrow keys resize the panels.

<Example name="resizable" variant="extra" />

## API reference

### Initialization

#### `create(scope?)`

Auto-discover and bind all resizable groups in a scope (defaults to `document`).

```typescript
import { create } from "@data-slot/resizable";

const controllers = create(); // Returns ResizableController[]
```

#### `createResizable(root, options?)`

Create a controller for a specific element.

```typescript
import { createResizable } from "@data-slot/resizable";

const resizable = createResizable(element, {
  direction: "horizontal",
  keyboardResizeBy: 10,
  onLayoutChange: (layout) => console.log(layout),
});
```

### Slots

| Slot | Description |
| ---- | ----------- |
| `resizable` | Root flex container for the group |
| `resizable-panel` | A panel with a percentage size |
| `resizable-handle` | Focusable separator between adjacent panels |

```html
<div data-slot="resizable">
  <div data-slot="resizable-panel">A</div>
  <div data-slot="resizable-handle" aria-label="Resize panels"></div>
  <div data-slot="resizable-panel">B</div>
</div>
```

A group needs at least one `resizable-panel`, and exactly one
`resizable-handle` between each adjacent pair of panes.

### Data Attributes

Options can also be set via data attributes. JS options take precedence.

On the root:

| Attribute                 | Type   | Default      | Description                      |
| ------------------------- | ------ | ------------ | -------------------------------- |
| `data-direction`          | string | `horizontal` | Layout axis                      |
| `data-keyboard-resize-by` | number | `10`         | Percent moved per arrow keypress |

On each `resizable-panel`:

| Attribute             | Type    | Default    | Description             |
| --------------------- | ------- | ---------- | ----------------------- |
| `data-default-size`   | number  | even split | Initial size (%)        |
| `data-min-size`       | number  | `0`        | Minimum size (%)        |
| `data-max-size`       | number  | `100`      | Maximum size (%)        |
| `data-collapsible`    | boolean | `false`    | Pane can collapse       |
| `data-collapsed-size` | number  | `0`        | Size (%) when collapsed |

Panel sizes and constraints must be between 0 and 100. `minSize` must not exceed
`maxSize`, and a collapsible panel's `collapsedSize` must not exceed `minSize`.
The constraints must allow a layout totaling 100%; initialization throws if they
cannot. A collapsible panel can occupy its collapsed size or any size between
its minimum and maximum.

### Options

| Option             | Type                         | Default        | Description                      |
| ------------------ | ---------------------------- | -------------- | -------------------------------- |
| `direction`        | `"horizontal" \| "vertical"` | `"horizontal"` | Layout axis                      |
| `keyboardResizeBy` | `number`                     | `10`           | Percent moved per arrow keypress |
| `onLayoutChange`   | `(layout: number[]) => void` | `undefined`    | Called when the layout changes   |

### Controller

Pane indices start at zero. `setLayout()` requires one finite, non-negative number
per panel and a positive total; it normalizes the values to 100% and applies the
panel constraints. Invalid layouts throw without changing the current layout.
`resizePane()` requires a finite size. Indexed mutation methods and `getSize()`
throw for an invalid pane index.

| Method/Property           | Description                                            |
| ------------------------- | ------------------------------------------------------ |
| `layout`                  | Current layout as `number[]` of percentages (readonly) |
| `setLayout(sizes)`        | Set the full layout (validated/clamped)                |
| `resizePane(index, size)` | Resize a pane to `size`%                               |
| `collapse(index)`         | Collapse a collapsible pane                            |
| `expand(index)`           | Expand a collapsed pane                                |
| `isCollapsed(index)`      | Whether a pane is collapsed                            |
| `isExpanded(index)`       | Whether a pane is expanded                             |
| `getSize(index)`          | Current size (%) of a pane                             |
| `destroy()`               | Cleanup all listeners and global styles                |

### Events

#### Outbound Events

```javascript
element.addEventListener("resizable:change", (e) => {
  console.log("Layout:", e.detail.layout);
});

element.addEventListener("resizable:dragging", (e) => {
  console.log("Dragging:", e.detail.dragging);
});
```

#### Inbound Events

| Event           | Detail                 | Description                     |
| --------------- | ---------------------- | ------------------------------- |
| `resizable:set` | `{ layout: number[] }` | Set the layout programmatically |

```javascript
element.dispatchEvent(new CustomEvent("resizable:set", { detail: { layout: [30, 70] } }));
```

### Styling

The component sets `flex` styles on the root and panels directly. Give the group
a height and the handles a visible width or height. Use `data-*` attributes for
visual styling:

```css
/* Style the handle */
[data-slot="resizable-handle"] {
  width: 4px;
  background: #ccc;
}

[data-slot="resizable"][data-direction="vertical"] [data-slot="resizable-handle"] {
  width: 100%;
  height: 4px;
}

/* Active drag / keyboard focus */
[data-slot="resizable-handle"][data-active] {
  background: #2563eb;
}

/* Collapsed pane */
[data-slot="resizable-panel"][data-collapsed] {
  opacity: 0;
}
```

Add a CSS transition on `flex-grow` for animated collapse/expand:

```css
[data-slot="resizable-panel"] {
  transition: flex-grow 0.2s ease;
}
```

### Keyboard Navigation

Focus a handle with `Tab` before using these keys:

| Key | Action |
| --- | ------ |
| `ArrowLeft` / `ArrowRight` | Resize a horizontal group by `keyboardResizeBy` (10% by default) |
| `ArrowUp` / `ArrowDown` | Resize a vertical group by the same step |
| `Shift` + arrow key | Move to the limit in that direction |
| `Home` / `End` | Minimize / maximize the preceding panel within constraints |
| `Enter` | Toggle collapse of the preceding panel when it is collapsible |
| `F6` / `Shift` + `F6` | Focus the next / previous handle, wrapping within the group |

### Accessibility

The component automatically handles:

- `role="separator"` on each handle
- `aria-orientation` (perpendicular to the layout direction)
- `aria-controls` linking each handle to its preceding pane
- `aria-valuemin` / `aria-valuemax` / `aria-valuenow` reflecting live constraints
- Unique ID generation for the root, panes, and handles

Give each handle an accessible name with `aria-label` or `aria-labelledby`.

### Behavior

#### Switching input methods

Keyboard resizing and valid imperative layout updates end an active drag before
applying the change. Subsequent pointer movement has no effect until a new drag
starts. Only one group can drag at a time in each document.

All input methods remember a panel's size immediately before it collapses.
`expand()` and `Enter` restore that size, subject to the other panels' constraints.
An initially collapsed panel expands to its minimum size when no prior size exists.

#### Persisting Layout

There is no built-in persistence, but reading and restoring the split is a
one-liner:

```javascript
import { createResizable } from "@data-slot/resizable";

const el = document.querySelector('[data-slot="resizable"]');
const saved = localStorage.getItem("layout:sidebar");

const resizable = createResizable(el, {
  onLayoutChange(layout) {
    localStorage.setItem("layout:sidebar", JSON.stringify(layout));
  },
});

if (saved) resizable.setLayout(JSON.parse(saved));
```
