Resizable
Panels that share space and resize with a draggable, keyboard-accessible handle.
@data-slot/resizableSource ↗bun add @data-slot/resizablenpm install @data-slot/resizablepnpm add @data-slot/resizableAnatomy
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.
<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.
Panel A
Drag the handle to resize.
Panel B
Keyboard: arrow keys adjust size.
Panel A
Drag the handle to resize.
Panel B
Keyboard: arrow keys adjust size.
Show code
<div data-slot="resizable" class="resizable-root">
<div data-slot="resizable-panel" data-default-size="50" data-min-size="20" class="resizable-panel resizable-panel-dark">
<p><strong>Panel A</strong></p>
<p>Drag the handle to resize.</p>
</div>
<div data-slot="resizable-handle" aria-label="Resize panels" class="resizable-handle"></div>
<div data-slot="resizable-panel" data-default-size="50" data-min-size="20" class="resizable-panel resizable-panel-light">
<p><strong>Panel B</strong></p>
<p>Keyboard: arrow keys adjust size.</p>
</div>
</div>
<style>
.resizable-root {
display: flex;
flex-direction: row;
width: 100%;
height: 10rem;
border: 1px solid var(--border);
border-radius: 0;
overflow: hidden;
}
.resizable-root[data-direction="vertical"] {
height: 16rem;
}
.resizable-panel {
padding: 1rem;
background: var(--code-bg);
overflow: auto;
}
.resizable-panel-light {
background: var(--surface);
}
.resizable-panel p {
margin: 0 0 0.5rem;
font-size: 0.875rem;
color: var(--muted);
}
.resizable-panel p strong {
color: var(--text);
}
.resizable-handle {
width: 6px;
background: var(--border);
cursor: col-resize;
flex-shrink: 0;
transition: background 0.15s;
}
.resizable-handle[data-direction="vertical"] {
width: 100%;
height: 6px;
cursor: row-resize;
}
.resizable-handle:hover,
.resizable-handle[data-active] {
background: var(--text);
}
.resizable-handle:focus-visible {
outline: 2px solid var(--text);
outline-offset: -2px;
}
</style><div data-slot="resizable" class="flex w-full h-40 border border-[var(--border)] overflow-hidden data-[direction=vertical]:h-64">
<div data-slot="resizable-panel" data-default-size="50" data-min-size="20" class="p-4 bg-code-bg overflow-auto">
<p class="text-sm text-[var(--muted)] mb-1"><strong class="text-[var(--text)]">Panel A</strong></p>
<p class="text-sm text-[var(--muted)]">Drag the handle to resize.</p>
</div>
<div
data-slot="resizable-handle" aria-label="Resize panels"
class="w-1.5 shrink-0 bg-[var(--border)] cursor-col-resize transition-colors data-[direction=vertical]:w-full data-[direction=vertical]:h-1.5 data-[direction=vertical]:cursor-row-resize
hover:bg-[var(--text)] data-[active]:bg-[var(--text)]
focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--text)] focus-visible:outline-offset-[-2px]"
></div>
<div data-slot="resizable-panel" data-default-size="50" data-min-size="20" class="p-4 bg-[var(--surface)] overflow-auto">
<p class="text-sm text-[var(--muted)] mb-1"><strong class="text-[var(--text)]">Panel B</strong></p>
<p class="text-sm text-[var(--muted)]">Keyboard: arrow keys adjust size.</p>
</div>
</div>import { create } from "@data-slot/resizable";
// Initialize after the markup is in the document.
const controllers = create();
// Clean up before removing the component.
// controllers.forEach((controller) => controller.destroy());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.
Panel A
Drag the handle to resize.
Panel B
Keyboard: arrow keys adjust size.
Panel A
Drag the handle to resize.
Panel B
Keyboard: arrow keys adjust size.
Show code
<div data-slot="resizable" data-direction="vertical" class="resizable-root">
<div data-slot="resizable-panel" data-default-size="50" data-min-size="20" class="resizable-panel resizable-panel-dark">
<p><strong>Panel A</strong></p>
<p>Drag the handle to resize.</p>
</div>
<div data-slot="resizable-handle" aria-label="Resize panels" class="resizable-handle"></div>
<div data-slot="resizable-panel" data-default-size="50" data-min-size="20" class="resizable-panel resizable-panel-light">
<p><strong>Panel B</strong></p>
<p>Keyboard: arrow keys adjust size.</p>
</div>
</div>
<style>
.resizable-root {
display: flex;
flex-direction: row;
width: 100%;
height: 10rem;
border: 1px solid var(--border);
border-radius: 0;
overflow: hidden;
}
.resizable-root[data-direction="vertical"] {
height: 16rem;
}
.resizable-panel {
padding: 1rem;
background: var(--code-bg);
overflow: auto;
}
.resizable-panel-light {
background: var(--surface);
}
.resizable-panel p {
margin: 0 0 0.5rem;
font-size: 0.875rem;
color: var(--muted);
}
.resizable-panel p strong {
color: var(--text);
}
.resizable-handle {
width: 6px;
background: var(--border);
cursor: col-resize;
flex-shrink: 0;
transition: background 0.15s;
}
.resizable-handle[data-direction="vertical"] {
width: 100%;
height: 6px;
cursor: row-resize;
}
.resizable-handle:hover,
.resizable-handle[data-active] {
background: var(--text);
}
.resizable-handle:focus-visible {
outline: 2px solid var(--text);
outline-offset: -2px;
}
</style><div data-slot="resizable" data-direction="vertical" class="flex w-full h-40 border border-[var(--border)] overflow-hidden data-[direction=vertical]:h-64">
<div data-slot="resizable-panel" data-default-size="50" data-min-size="20" class="p-4 bg-code-bg overflow-auto">
<p class="text-sm text-[var(--muted)] mb-1"><strong class="text-[var(--text)]">Panel A</strong></p>
<p class="text-sm text-[var(--muted)]">Drag the handle to resize.</p>
</div>
<div
data-slot="resizable-handle" aria-label="Resize panels"
class="w-1.5 shrink-0 bg-[var(--border)] cursor-col-resize transition-colors data-[direction=vertical]:w-full data-[direction=vertical]:h-1.5 data-[direction=vertical]:cursor-row-resize
hover:bg-[var(--text)] data-[active]:bg-[var(--text)]
focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--text)] focus-visible:outline-offset-[-2px]"
></div>
<div data-slot="resizable-panel" data-default-size="50" data-min-size="20" class="p-4 bg-[var(--surface)] overflow-auto">
<p class="text-sm text-[var(--muted)] mb-1"><strong class="text-[var(--text)]">Panel B</strong></p>
<p class="text-sm text-[var(--muted)]">Keyboard: arrow keys adjust size.</p>
</div>
</div>import { create } from "@data-slot/resizable";
// Initialize after the markup is in the document.
const controllers = create();
// Clean up before removing the component.
// controllers.forEach((controller) => controller.destroy());API reference
Initialization
create(scope?)
Auto-discover and bind all resizable groups in a scope (defaults to document).
import { create } from "@data-slot/resizable";
const controllers = create(); // Returns ResizableController[]
createResizable(root, options?)
Create a controller for a specific element.
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 |
<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
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 |
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:
/* 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:
[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 handlearia-orientation(perpendicular to the layout direction)aria-controlslinking each handle to its preceding panearia-valuemin/aria-valuemax/aria-valuenowreflecting 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:
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));