Skip to content

Resizable

Accessible resizable panel groups with keyboard support. Built on the @areia/slots resizable primitive.

Source Code

Resizable wires panel sizing, drag handles, keyboard navigation, and layout change events.

Panel sizes and ilha morph

defaultSize is a percentage of the group (values should sum to about 100). When only some panels set defaultSize, Areia fills missing siblings with the remaining space in markup so SSR flex-grow stays coherent (e.g. 20 + omitted → 20 / 80, not 20 / 1).

After mount, @areia/slots owns each panel’s inline style (flex-grow percentages) and the root’s flex container styles. Under ilha, parent island re-renders morph the tree from template markup. Resizable stamps data-morph-preserve="style" on the root, panels, and handles by default (merged with any user data-morph-preserve tokens) so dragged sizes survive filters, loaders, and nested layout updates. You do not need to add data-morph-preserve="style" yourself.

Nested groups (shell sidebar + detail split) work the same way. Persist layout across full remounts with onLayoutChange if you want — that is optional app state, not required for morph safety.

Left
Right
import ilha from "ilha";
import { Resizable } from "areia";

export default ilha.render(() => (
  <Resizable
    direction="horizontal"
    class="h-64 max-w-md rounded-lg border border-areia-border"
  >
    <Resizable.Panel defaultSize={50} minSize={10} collapsible>
      <div class="flex items-center justify-center p-4">
        <span class="font-semibold">Left</span>
      </div>
    </Resizable.Panel>
    <Resizable.Handle withHandle />
    <Resizable.Panel defaultSize={50} minSize={10}>
      <div class="flex items-center justify-center p-4">
        <span class="font-semibold">Right</span>
      </div>
    </Resizable.Panel>
  </Resizable>
));

Import

import { Resizable } from "areia";

Usage

Compose panels and handles within a root group. The root must be a direct parent of alternating panels and handles.

Left
Right
import ilha from "ilha";
import { Resizable } from "areia";

export default ilha.render(() => (
  <Resizable
    direction="horizontal"
    class="h-64 max-w-md rounded-lg border border-areia-border"
  >
    <Resizable.Panel defaultSize={50}>
      <div class="flex items-center justify-center p-4">
        <span class="font-semibold">Left</span>
      </div>
    </Resizable.Panel>
    <Resizable.Handle />
    <Resizable.Panel defaultSize={50}>
      <div class="flex items-center justify-center p-4">
        <span class="font-semibold">Right</span>
      </div>
    </Resizable.Panel>
  </Resizable>
));

Composition

Use the following composition to build a resizable panel group:

Resizable (root)
├── Resizable.Panel
├── Resizable.Handle
└── Resizable.Panel

Examples

Vertical

Use direction="vertical" for vertical resizing.

Header
Content
import ilha from "ilha";
import { Resizable } from "areia";

export default ilha.render(() => (
  <Resizable
    direction="vertical"
    class="h-64 max-w-md rounded-lg border border-areia-border"
  >
    <Resizable.Panel defaultSize={25}>
      <div class="flex items-center justify-center p-4">
        <span class="font-semibold">Header</span>
      </div>
    </Resizable.Panel>
    <Resizable.Handle />
    <Resizable.Panel defaultSize={75}>
      <div class="flex items-center justify-center p-4">
        <span class="font-semibold">Content</span>
      </div>
    </Resizable.Panel>
  </Resizable>
));

With Handle

Use withHandle on Resizable.Handle to show a visible grip indicator.

Sidebar
Content
import ilha from "ilha";
import { Resizable } from "areia";

export default ilha.render(() => (
  <Resizable
    direction="horizontal"
    class="h-64 max-w-md rounded-lg border border-areia-border"
  >
    <Resizable.Panel defaultSize={25}>
      <div class="flex items-center justify-center p-4">
        <span class="font-semibold">Sidebar</span>
      </div>
    </Resizable.Panel>
    <Resizable.Handle withHandle />
    <Resizable.Panel defaultSize={75}>
      <div class="flex items-center justify-center p-4">
        <span class="font-semibold">Content</span>
      </div>
    </Resizable.Panel>
  </Resizable>
));

Nested Panels

Resizable panels can be nested for complex layouts.

One
Two
Three
import ilha from "ilha";
import { Resizable } from "areia";

export default ilha.render(() => (
  <Resizable
    direction="horizontal"
    class="h-64 max-w-md rounded-lg border border-areia-border"
  >
    <Resizable.Panel defaultSize={50}>
      <div class="flex items-center justify-center p-4">
        <span class="font-semibold">One</span>
      </div>
    </Resizable.Panel>
    <Resizable.Handle withHandle />
    <Resizable.Panel defaultSize={50}>
      <Resizable direction="vertical">
        <Resizable.Panel defaultSize={25}>
          <div class="flex items-center justify-center p-4">
            <span class="font-semibold">Two</span>
          </div>
        </Resizable.Panel>
        <Resizable.Handle withHandle />
        <Resizable.Panel defaultSize={75}>
          <div class="flex items-center justify-center p-4">
            <span class="font-semibold">Three</span>
          </div>
        </Resizable.Panel>
      </Resizable>
    </Resizable.Panel>
  </Resizable>
));

API Reference

Resizable (ResizablePanelGroup)

Root component that manages the resizable group state.

PropTypeDefaultDescription
childrenunknownAlternating panels and handles.
direction"horizontal" | "vertical""horizontal"Layout axis.
keyboardResizeBynumber10Amount (in %) to resize by per keyboard arrow press.
onLayoutChange(layout: number[]) => voidCalled whenever the layout changes, with sizes as percentages.

Resizable.Panel

Individual panel within a resizable group.

PropTypeDefaultDescription
childrenunknownPanel content.
defaultSizenumberInitial size as a percentage.
minSizenumber0Minimum size as a percentage.
maxSizenumber100Maximum size as a percentage.
collapsiblebooleanfalseWhether the panel can be collapsed.
collapsedSizenumber0Size when collapsed as a percentage.

Resizable.Handle

Resize handle between two panels.

PropTypeDefaultDescription
withHandlebooleanfalseWhether to show a visible grip indicator.
childrenunknownCustom handle content.

Slots

Resizable renders these data slots:

  • resizable
  • resizable-panel
  • resizable-handle

The library sets state attributes on parts:

AttributeElementDescription
data-directionall parts"horizontal" or "vertical".
data-statepanel"expanded" or "collapsed".
data-collapsedpanelPresent when panel is collapsed.
data-expandedpanelPresent when panel is expanded.
data-activehandle"pointer" during drag, "keyboard" when focused.

Events

Outbound

EventDetailDescription
resizable:change{ layout: number[] }Fired when the layout changes.
resizable:dragging{ dragging: boolean }Fired when dragging starts or ends.

Inbound

EventDetailDescription
resizable:set{ layout: number[] }Set the layout programmatically.