Skip to content
data-slotv1.0.0
Esc
↑↓navigate↵open⌘Jpreview
On this page

Resizable

Panels that share space and resize with a draggable, keyboard-accessible handle.

@data-slot/resizableSource ↗
bun add @data-slot/resizable
npm install @data-slot/resizable
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.

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

Preview

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 codeHide 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>

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.

Preview

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 codeHide 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>

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 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:

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