# Resizable

Resize, collapse, and save panel layouts with pointer and keyboard controls.

Use a resizable group to let people size the panes of a layout, like a file list beside an editor, by dragging the handle between them or with the keyboard.

```svelte title="basic.svelte"
<script lang="ts">
  import {
    Resizable,
    ResizableHandle,
    ResizablePanel,
  } from "@ui-sdk/svelte/resizable"
</script>

<Resizable>
  <ResizablePanel defaultSize={35} minSize={20}>
    <h3>Files</h3>
    <ul>
      <li>page.tsx</li>
      <li>layout.tsx</li>
      <li>globals.css</li>
    </ul>
  </ResizablePanel>
  <ResizableHandle aria-label="Resize files" />
  <ResizablePanel defaultSize={65} minSize={40}>
    <Resizable orientation="vertical">
      <ResizablePanel defaultSize={60} minSize={30}>
        <h3>page.tsx</h3>
        <span data-slot="hint">Saved just now</span>
      </ResizablePanel>
      <ResizableHandle aria-label="Resize editor" />
      <ResizablePanel defaultSize={40} minSize={30}>
        <h3>Terminal</h3>
        <span data-slot="hint">Ready on port 3000</span>
      </ResizablePanel>
    </Resizable>
  </ResizablePanel>
</Resizable>
```

Drag the handle beside Files, then the one between page.tsx and Terminal.

## Installation

```bash
npm install @ui-sdk/svelte @ui-sdk/core
```

## Composition

Use the following composition to build a `Resizable`:

```text
Resizable
├── ResizablePanel
├── ResizableHandle
└── ResizablePanel
```

Put a handle between the panels you want to resize, and name it with `aria-label`, like "Resize files". The group lays its panels out in a row, so give it a size, and each panel's size is a percentage of the space the panels share. A group can sit inside a panel, like an editor above a terminal.

## Usage

Import the parts and put them together:

```svelte
<script lang="ts">
  import {
    Resizable,
    ResizableHandle,
    ResizablePanel,
  } from "@ui-sdk/svelte/resizable"
</script>

<Resizable>
  <ResizablePanel defaultSize={30}>Files</ResizablePanel>
  <ResizableHandle aria-label="Resize files" />
  <ResizablePanel>Editor</ResizablePanel>
</Resizable>
```

The panel without `defaultSize` takes what's left, here 70%.

## Build Your Own

`@ui-sdk/svelte/resizable` comes with a ready-made Resizable. If you'd like to build your own, with your own names, start from the `resizable` primitive in `@ui-sdk/core`:

```ts title="split-view.ts"
import { resizable } from "@ui-sdk/core/resizable"
import { createParts } from "@ui-sdk/svelte/primitive"

const {
  Root: SplitView,
  Panel: SplitViewPane,
  Handle: SplitViewDivider,
} = createParts(resizable, { root: "div", panel: "div", handle: "div" })

export { SplitView, SplitViewDivider, SplitViewPane }
```

```svelte
<SplitView>
  <SplitViewPane defaultSize={30}>Files</SplitViewPane>
  <SplitViewDivider aria-label="Resize files" />
  <SplitViewPane>Editor</SplitViewPane>
</SplitView>
```

Each part keeps the behavior, state and data attributes of its ready-made counterpart: the root lays the panels out, each pane writes its size to `--resizable-panel-size`, and the divider is the focusable separator. See [Rendering](/docs/svelte/rendering) and [Custom Parts](/docs/svelte/custom-parts) for more.

## Vertical

Set `orientation="vertical"` to stack the panels, and give the group a height. ArrowUp and ArrowDown move the handle. Every part gets `data-orientation` with the group's direction, so a vertical group's handle is `[data-slot="resizable-handle"][data-orientation="vertical"]`.

```svelte title="vertical.svelte"
<script lang="ts">
  import {
    Resizable,
    ResizableHandle,
    ResizablePanel,
  } from "@ui-sdk/svelte/resizable"
</script>

<Resizable orientation="vertical">
  <ResizablePanel defaultSize={45} minSize={25}>
    <h3>Inbox</h3>
    <span data-slot="hint">3 unread messages</span>
  </ResizablePanel>
  <ResizableHandle aria-label="Resize inbox" />
  <ResizablePanel defaultSize={55} minSize={25}>
    <h3>Quarterly report</h3>
    <span data-slot="hint">From Hollis Grant</span>
  </ResizablePanel>
</Resizable>
```

Drag the handle between Inbox and Quarterly report, or focus it and use ArrowUp and ArrowDown.

## Handle

Put anything inside a handle, like a grip that shows it can be dragged. The handle can stay a thin line: its hit area is at least 10 pixels across, centered on it, and 24 for touch, without any styles.

```svelte title="handle.svelte"
<script lang="ts">
  import {
    Resizable,
    ResizableHandle,
    ResizablePanel,
  } from "@ui-sdk/svelte/resizable"
</script>

<Resizable>
  <ResizablePanel defaultSize={35} minSize={20}>
    <h3>Channels</h3>
    <ul>
      <li>general</li>
      <li>design</li>
      <li>releases</li>
    </ul>
  </ResizablePanel>
  <ResizableHandle aria-label="Resize channels">
    <div></div>
  </ResizableHandle>
  <ResizablePanel defaultSize={65} minSize={20}>
    <h3>general</h3>
    <span data-slot="hint">Kofi Mensah: The release notes are ready.</span>
  </ResizablePanel>
</Resizable>
```

Grab the grip beside Channels, or focus the handle and use the arrow keys.

## Collapsible

`collapsible` lets a panel collapse below its `minSize` to `collapsedSize`, 0 by default: by dragging past the midpoint to its collapsed size, with Enter or a double-click on its handle, or through the controller. Dragging back past the same point opens it at its minimum, and Enter restores the size it had. A collapsed panel gets `data-collapsed`, and at size 0 its content leaves the tab order and the accessibility tree.

```svelte title="collapsible.svelte"
<script lang="ts">
  import { FolderIcon, SearchIcon, SettingsIcon } from "@lucide/svelte"
  import {
    Resizable,
    ResizableHandle,
    ResizablePanel,
  } from "@ui-sdk/svelte/resizable"
</script>

<Resizable>
  <ResizablePanel
    defaultSize={30}
    minSize={25}
    collapsible
    collapsedSize="48px"
  >
    <ul>
      <li data-slot="row">
        <FolderIcon aria-hidden="true" />
        <span>Files</span>
      </li>
      <li data-slot="row">
        <SearchIcon aria-hidden="true" />
        <span>Search</span>
      </li>
      <li data-slot="row">
        <SettingsIcon aria-hidden="true" />
        <span>Settings</span>
      </li>
    </ul>
  </ResizablePanel>
  <ResizableHandle aria-label="Resize sidebar">
    <div></div>
  </ResizableHandle>
  <ResizablePanel minSize={40}>
    <h3>README.md</h3>
    <span data-slot="hint">Edited by Inès Moreau</span>
  </ResizablePanel>
</Resizable>
```

Drag the sidebar closed to leave only its icons, or focus its handle and press Enter to collapse and restore it.

## Sizes in Pixels

Sizes are percentages as numbers, or strings with `%`, `px` or `rem`. Pixel and rem bounds hold as the group resizes, so a navigation with `minSize="160px"` never gets narrower, and a change they cause reports the `resize` reason.

```svelte title="sizes.svelte"
<script lang="ts">
  import {
    Resizable,
    ResizableHandle,
    ResizablePanel,
  } from "@ui-sdk/svelte/resizable"
</script>

<Resizable>
  <ResizablePanel defaultSize="200px" minSize="160px" maxSize="320px">
    <h3>Docs</h3>
    <ul>
      <li>Getting Started</li>
      <li>Components</li>
      <li>Guides</li>
    </ul>
  </ResizablePanel>
  <ResizableHandle aria-label="Resize navigation">
    <div></div>
  </ResizableHandle>
  <ResizablePanel minSize={30}>
    <h3>Getting Started</h3>
    <span data-slot="hint">Install the packages</span>
  </ResizablePanel>
</Resizable>
```

Resize Docs. It stays between 160 and 320 pixels wide, whatever the width of the group.

## Default Layout

`defaultLayout` sets every panel's size at once, in order. Otherwise panels without `defaultSize` share what's left evenly.

```svelte title="default-layout.svelte"
<script lang="ts">
  import {
    Resizable,
    ResizableHandle,
    ResizablePanel,
  } from "@ui-sdk/svelte/resizable"
</script>

<Resizable defaultLayout={[25, 35, 40]}>
  <ResizablePanel minSize={15}>
    <h3>Mail</h3>
    <ul>
      <li>Inbox</li>
      <li>Drafts</li>
      <li>Sent</li>
    </ul>
  </ResizablePanel>
  <ResizableHandle aria-label="Resize folders" />
  <ResizablePanel minSize={15}>
    <h3>Inbox</h3>
    <ul>
      <li>Hollis Grant</li>
      <li>Inès Moreau</li>
      <li>Kofi Mensah</li>
    </ul>
  </ResizablePanel>
  <ResizableHandle aria-label="Resize inbox" />
  <ResizablePanel minSize={15}>
    <h3>Quarterly report</h3>
    <span data-slot="hint">From Hollis Grant</span>
  </ResizablePanel>
</Resizable>
```

The three mail panels start at 25, 35 and 40 percent. Resize any of them.

## Controlled

Bind `layout` with `bind:layout` to control the group, and pass `onLayoutChange` for the reason. The layout is one percentage per panel, in order, adding up to 100. Every change carries a [reason](#reasons), like `drag`, `step-key` or `collapse`, and `details.cancel()` keeps the current layout.

```svelte title="controlled.svelte"
<script lang="ts">
  import {
    Resizable,
    ResizableHandle,
    ResizablePanel,
  } from "@ui-sdk/svelte/resizable"
  import { useLog } from "./log"

  const format = (layout: number[]) => layout.map(Math.round).join(" / ")

  const log = useLog()
  let layout = $state([30, 70])
</script>

<div data-slot="stack">
  <div data-slot="controls">
    <button type="button" onclick={() => (layout = [50, 50])}
      >Split Evenly</button
    >
    <button type="button" onclick={() => (layout = [30, 70])}>Reset</button>
  </div>
  <Resizable
    bind:layout
    onLayoutChange={(next, details) => {
      if (format(next) !== format(layout)) {
        log(`${format(next)} (${details.reason})`)
      }
    }}
  >
    <ResizablePanel minSize={20}>
      <h3>Files</h3>
      <ul>
        <li>page.tsx</li>
        <li>layout.tsx</li>
        <li>globals.css</li>
      </ul>
    </ResizablePanel>
    <ResizableHandle aria-label="Resize files" />
    <ResizablePanel minSize={20}>
      <h3>page.tsx</h3>
      <span data-slot="hint">Saved just now</span>
    </ResizablePanel>
  </Resizable>
</div>
```

Resize Files to log the layout and its reason. Split Evenly and Reset set the layout from outside.

## Saving on Complete

`onLayoutChange` runs on every frame of a drag. `onLayoutCommit` runs once, when the interaction that changed the layout ends: the pointer lifts, the key comes up, or a panel collapses. Save there.

```svelte title="complete.svelte"
<script lang="ts">
  import {
    Resizable,
    ResizableHandle,
    ResizablePanel,
  } from "@ui-sdk/svelte/resizable"
  import { useLog } from "./log"

  const log = useLog()
</script>

<Resizable
  onLayoutCommit={(layout, details) =>
    log(`Saved ${layout.map(Math.round).join(" / ")} (${details.reason})`)}
>
  <ResizablePanel defaultSize={40} minSize={20}>
    <h3>Files</h3>
    <ul>
      <li>page.tsx</li>
      <li>layout.tsx</li>
      <li>globals.css</li>
    </ul>
  </ResizablePanel>
  <ResizableHandle aria-label="Resize files" />
  <ResizablePanel minSize={20}>
    <h3>page.tsx</h3>
    <span data-slot="hint">Saved just now</span>
  </ResizablePanel>
</Resizable>
```

Drag the handle and release it, or press an arrow key. The console logs the layout once, when you let go.

## Persistence

`autoSaveId` saves the layout in `localStorage` when an interaction completes and restores it when the group attaches in the browser. Server markup starts with the default sizes. Each set of panels keeps its own layout, so a panel that comes and goes gets its sizes back. Give panels an `id` so the saved layout survives changes elsewhere on the page.

It's the same idea as [Preferences](/docs/svelte/components/preferences), which saves one choice between visits, like a theme. Resizable saves the whole layout, under its `autoSaveId`.

```svelte title="persistence.svelte"
<script lang="ts">
  import {
    Resizable,
    ResizableHandle,
    ResizablePanel,
  } from "@ui-sdk/svelte/resizable"
</script>

<Resizable autoSaveId="docs-persistence">
  <ResizablePanel id="docs-sidebar" defaultSize={30} minSize={20}>
    <h3>Outline</h3>
    <ul>
      <li>Introduction</li>
      <li>Installation</li>
      <li>Usage</li>
    </ul>
  </ResizablePanel>
  <ResizableHandle aria-label="Resize outline">
    <div></div>
  </ResizableHandle>
  <ResizablePanel id="docs-content" minSize={30}>
    <h3>Introduction</h3>
    <span data-slot="hint">Last edited by Wren Takahashi</span>
  </ResizablePanel>
</Resizable>
```

Resize Outline, then reload this page to restore its saved width.

## Storage

`storage` takes anything with `getItem` and `setItem`, like `sessionStorage` or a cookie. That's the browser's storage shape, not the [storage](/docs/svelte/components/preferences#save-the-value) helpers Preferences takes, such as `createLocalStorage`, which each keep one value. Choose between local storage, session storage and a cookie as you would for a preference, under [Remember the Choice](/docs/core/behaviors/preferences#remember-the-choice).

This example keeps the layout in `sessionStorage`, for the tab's session:

```svelte title="storage.svelte"
<script lang="ts">
  import {
    Resizable,
    ResizableHandle,
    ResizablePanel,
    type ResizableStorage,
  } from "@ui-sdk/svelte/resizable"

  const session: ResizableStorage = {
    getItem: (key) => sessionStorage.getItem(key),
    setItem: (key, value) => sessionStorage.setItem(key, value),
  }
</script>

<Resizable autoSaveId="docs-storage" storage={session}>
  <ResizablePanel id="session-chats" defaultSize={35} minSize={20}>
    <h3>Messages</h3>
    <ul>
      <li>Inès Moreau</li>
      <li>Kofi Mensah</li>
      <li>Wren Takahashi</li>
    </ul>
  </ResizablePanel>
  <ResizableHandle aria-label="Resize messages" />
  <ResizablePanel id="session-thread" minSize={20}>
    <h3>Inès Moreau</h3>
    <span data-slot="hint">Can we move the review to Friday?</span>
  </ResizablePanel>
</Resizable>
```

Resize Messages, then reload. The width comes back in this tab, but not in a new one.

A server can't read `localStorage`, so it renders each panel's `defaultSize` and the saved layout arrives when the page hydrates. To render the saved layout on the first paint, keep it in a cookie and pass the sizes back as each panel's `defaultSize`. Here a SvelteKit page reads the cookie on the server. It imports `resizableStorageKey`, which names the cookie:

```ts title="src/routes/+page.server.ts"
import { resizableStorageKey } from "@ui-sdk/svelte/resizable"

export function load({ cookies }) {
  const saved = JSON.parse(cookies.get(resizableStorageKey("editor")) ?? "{}")
  const [sidebar = 25, main = 75] = saved["sidebar,main"] ?? []
  return { sidebar, main }
}
```

```svelte title="src/routes/+page.svelte"
<script lang="ts">
  import Editor from "./editor.svelte"

  let { data } = $props()
</script>

<Editor sidebar={data.sidebar} main={data.main} />
```

```svelte title="src/routes/editor.svelte"
<script lang="ts">
  import {
    Resizable,
    ResizableHandle,
    ResizablePanel,
    type ResizableStorage,
  } from "@ui-sdk/svelte/resizable"

  interface Props {
    sidebar: number
    main: number
  }

  let { sidebar, main }: Props = $props()

  const cookieStorage: ResizableStorage = {
    getItem: (key) => {
      const value = document.cookie.match(
        new RegExp(`(?:^|; )${key}=([^;]*)`)
      )?.[1]
      return value === undefined ? null : decodeURIComponent(value)
    },
    setItem: (key, value) => {
      document.cookie = `${key}=${encodeURIComponent(value)}; path=/; max-age=31536000`
    },
  }
</script>

<Resizable autoSaveId="editor" storage={cookieStorage}>
  <ResizablePanel id="sidebar" defaultSize={sidebar} />
  <ResizableHandle aria-label="Resize sidebar" />
  <ResizablePanel id="main" defaultSize={main} />
</Resizable>
```

## Controller

A controller, from `createResizableController()`, collapses, expands and resizes panels from anywhere, by id or index, and reads the layout with `getLayout()`. Changes it makes report the `imperative` reason.

```svelte title="controller.svelte"
<script lang="ts">
  import {
    createResizableController,
    Resizable,
    ResizableHandle,
    ResizablePanel,
  } from "@ui-sdk/svelte/resizable"
  import { useLog } from "./log"

  const log = useLog()
  const editor = createResizableController()

  function read() {
    log(
      `Layout ${editor.getLayout().map(Math.round).join(" / ")}${editor.isCollapsed("controller-sidebar") ? ", collapsed" : ""}`
    )
  }
</script>

<div data-slot="stack">
  <div data-slot="controls">
    <button type="button" onclick={() => editor.collapse("controller-sidebar")}
      >Collapse</button
    >
    <button type="button" onclick={() => editor.expand("controller-sidebar")}
      >Expand</button
    >
    <button
      type="button"
      onclick={() => editor.resize("controller-sidebar", "240px")}>240px</button
    >
    <button type="button" onclick={read}>Read</button>
  </div>
  <Resizable controller={editor}>
    <ResizablePanel
      id="controller-sidebar"
      defaultSize={30}
      minSize={20}
      collapsible
    >
      <h3>Files</h3>
      <ul>
        <li>page.tsx</li>
        <li>layout.tsx</li>
        <li>globals.css</li>
      </ul>
    </ResizablePanel>
    <ResizableHandle aria-label="Resize files" />
    <ResizablePanel minSize={30}>
      <h3>page.tsx</h3>
      <span data-slot="hint">Saved just now</span>
    </ResizablePanel>
  </Resizable>
</div>
```

Use Collapse, Expand and 240px to move Files from outside the group, and Read to log its sizes.

## Step Size

`step` sets how far an arrow key moves a handle, 1% by default, and `largeStep` how far Shift with an arrow moves it, 10% by default.

```svelte title="keyboard.svelte"
<script lang="ts">
  import {
    Resizable,
    ResizableHandle,
    ResizablePanel,
  } from "@ui-sdk/svelte/resizable"
</script>

<Resizable step={5} largeStep={25}>
  <ResizablePanel defaultSize={50} minSize={20}>
    <h3>Original</h3>
    <span data-slot="hint">v1.2.0</span>
  </ResizablePanel>
  <ResizableHandle aria-label="Resize original" />
  <ResizablePanel defaultSize={50} minSize={20}>
    <h3>Changes</h3>
    <span data-slot="hint">v1.3.0</span>
  </ResizablePanel>
</Resizable>
```

Focus the handle between Original and Changes. The arrow keys move it 5%, Shift with an arrow 25%, and Home and End as far as it goes.

## Disabled

`disabled` on a handle fixes it in place. On the group it fixes every handle. Disabled handles leave the tab order, and their panels keep their sizes. A disabled `<fieldset>` around the group fixes every handle too.

```svelte title="disabled.svelte"
<script lang="ts">
  import { CheckIcon } from "@lucide/svelte"
  import {
    Checkbox,
    CheckboxIndicator,
    type CheckedState,
  } from "@ui-sdk/svelte/checkbox"
  import {
    Resizable,
    ResizableHandle,
    ResizablePanel,
  } from "@ui-sdk/svelte/resizable"

  let locked = $state<CheckedState>(false)
</script>

<div data-slot="stack">
  <div data-slot="controls">
    <label data-slot="label">
      <Checkbox bind:checked={locked}>
        <CheckboxIndicator>
          <CheckIcon aria-hidden="true" />
        </CheckboxIndicator>
      </Checkbox>
      Lock layout
    </label>
  </div>
  <Resizable disabled={locked === true}>
    <ResizablePanel minSize={15}>
      <h3>Pinned</h3>
      <span data-slot="hint">2 notes</span>
    </ResizablePanel>
    <ResizableHandle disabled aria-label="Resize pinned" />
    <ResizablePanel minSize={15}>
      <h3>Inbox</h3>
      <span data-slot="hint">3 unread</span>
    </ResizablePanel>
    <ResizableHandle aria-label="Resize inbox" />
    <ResizablePanel minSize={15}>
      <h3>Message</h3>
      <span data-slot="hint">From Kofi Mensah</span>
    </ResizablePanel>
  </Resizable>
</div>
```

The handle after Pinned stays fixed. Drag the one after Inbox, then check Lock layout to fix it too.

## Custom Rendering

The `render` and `children` snippets receive the part's state, like a panel's `size` and whether it's `collapsed`, or whether a handle is `dragging` or `hovering`. See [Parts and Composition](/docs/svelte/parts-and-composition) for the rendering contract.

```svelte title="render.svelte"
<script lang="ts">
  import {
    Resizable,
    ResizableHandle,
    ResizablePanel,
  } from "@ui-sdk/svelte/resizable"
</script>

<Resizable>
  <ResizablePanel defaultSize={35} minSize={20} collapsible>
    {#snippet children(state)}
      <h3>Files</h3>
      <span data-slot="hint"
        >{state.collapsed ? "Collapsed" : `${Math.round(state.size)}%`}</span
      >
    {/snippet}
  </ResizablePanel>
  <ResizableHandle aria-label="Resize files">
    {#snippet render({ props })}
      <div {...props}></div>
    {/snippet}
  </ResizableHandle>
  <ResizablePanel minSize={30}>
    {#snippet children(state)}
      <h3>Editor</h3>
      <span data-slot="hint">{`${Math.round(state.size)}%`}</span>
    {/snippet}
  </ResizablePanel>
</Resizable>
```

Resize Files to see both panels report their size, or collapse it to see Collapsed.

The primitive writes drag sizes directly to panel styles. Reading `size` or `layout` in a snippet also updates that part on every frame of the drag.

## Styling

Parts are unstyled, apart from the layout they need: the group is a flex row or column, panels take their size as a flex basis, and handles don't shrink and take `touch-action: none`. Give the group a size, the handle a width or height and a background, and style it with `data-hovering`, `data-dragging` and `:focus-visible`. While a handle is hovered or dragged, its cursor shows over the whole page, and points the only way it can go at a limit.

Each part has a `data-slot`, such as `resizable-handle`, and its state attributes are listed under [Data Attributes](#data-attributes).

### ResizablePanel
- `--resizable-panel-size`: The panel's share of the group, as a percentage. Written on every frame of a drag, without a render.

## Data Attributes

### Resizable
`[data-slot="resizable"]`

- `data-orientation` (`horizontal`, `vertical`): The group's direction, on every part. A handle's own `aria-orientation` is the other one, since it runs across the group.
- `data-disabled`: Present when the group, or this handle, is disabled.
- `data-dragging`: Present while a handle of the group is being dragged.

### ResizablePanel
`[data-slot="resizable-panel"]`

- `data-orientation` (`horizontal`, `vertical`): The group's direction, on every part. A handle's own `aria-orientation` is the other one, since it runs across the group.
- `data-collapsed`: Present while a collapsible panel is collapsed.

### ResizableHandle
`[data-slot="resizable-handle"]`

- `data-orientation` (`horizontal`, `vertical`): The group's direction, on every part. A handle's own `aria-orientation` is the other one, since it runs across the group.
- `data-disabled`: Present when the group, or this handle, is disabled.
- `data-dragging`: Present while this handle is being dragged.
- `data-hovering`: Present while the pointer is over the handle or within its hit area, which reaches past the handle itself.

## API Reference

Props and callbacks are the same as below, as Svelte props. `layout` can be bound with `bind:layout`.

### Resizable

- `layout?` (`number[]`): Each panel's size in percent, in panel order, like `[25, 75]`. Pass it to control the group.
- `defaultLayout?` (`number[]`): The layout at first, when uncontrolled. Panels' own `defaultSize` is what a server renders, so prefer that with server rendering.
- `onLayoutChange?` (`((layout: number[], details: ResizableLayoutChangeDetails) => void)`): Runs while the layout changes: on every frame of a drag and every key. `details.cancel()` keeps the layout.
- `onLayoutCommit?` (`((layout: number[], details: ResizableLayoutCommitDetails) => void)`): Runs once an interaction that changed the layout ends: the pointer lifts, the key comes up, or a panel collapses. For saving.
- `orientation?` (`"horizontal" | "vertical"`): The direction panels are laid out in. Horizontal panels sit side by side, vertical ones stack.
- `disabled?` (`boolean`): Stops every handle: no drags, no keys, and handles leave the tab order.
- `step?` (`number`): How far an arrow key moves a handle, in percent.
- `largeStep?` (`number`): How far Shift with an arrow key moves a handle, in percent.
- `autoSaveId?` (`string`): Saves the layout when an interaction completes and restores it on the next visit, under this key. Give panels an `id` so it survives changes elsewhere on the page.
- `storage?` (`ResizableStorage`): Where `autoSaveId` saves: anything with `getItem` and `setItem`, like a cookie store a server can read.
- `controller?` (`object`): Resizes the group from anywhere. Create one with `createResizableController()`.
- `debug?` (`boolean`): Logs every state change with its reason to the console, in development builds.

### ResizablePanel

- `id?` (`string`): Identifies the panel for the controller, `aria-controls` and saved layouts. It's the element's id. Generated when left out.
- `defaultSize?` (`ResizableSize`): Where the panel starts, in percent or with a unit, like `30` or `"240px"`. Without one, panels share what's left evenly. A server renders this size.
- `minSize?` (`ResizableSize`): The smallest the panel gets while open.
- `maxSize?` (`ResizableSize`): The largest the panel gets.
- `collapsible?` (`boolean`): Lets the panel collapse below its minimum to `collapsedSize`: by dragging past half the way there, with Enter or a double-click on its handle, or through the controller.
- `collapsedSize?` (`ResizableSize`): The size a collapsible panel collapses to. At 0, its content leaves the tab order and the accessibility tree.

### ResizableHandle

- `disabled?` (`boolean`): Stops this handle: no drags and no keys, and it leaves the tab order.

### State

What render functions, state-based class names and slots receive.

- `orientation` (`"horizontal" | "vertical"`): The direction panels are laid out in.
- `disabled` (`boolean`): Whether the group is disabled.
- `dragging` (`boolean`): Whether a handle is being dragged.
- `layout` (`number[]`): Each panel's size in percent. Reading it renders on every frame of a drag.

Panels:

- `orientation` (`"horizontal" | "vertical"`): The direction panels are laid out in.
- `size` (`number`): The panel's size in percent. Reading it renders on every frame of a drag.
- `collapsed` (`boolean`): Whether the panel is down at its `collapsedSize`, below its minimum.
- `collapsible` (`boolean`): Whether the panel can collapse.

Handles:

- `orientation` (`"horizontal" | "vertical"`): The direction panels are laid out in. The separator itself runs across it.
- `disabled` (`boolean`): Whether the handle can't move.
- `dragging` (`boolean`): Whether the handle is being dragged.
- `hovering` (`boolean`): Whether the pointer is over the handle or within its hit area.

### Reasons

`details.reason` says why the layout changed.

- `drag` (`PointerEvent`): A slider thumb or a resize handle was dragged, or an item was dragged to a new place in a list.
- `step-key` (`KeyboardEvent`): Arrow, Page Up, Page Down, Home or End stepped a slider or a number input.
- `collapse` (`MouseEvent | KeyboardEvent`): Enter or a double-click on a resize handle collapsed the panel beside it, or restored it.
- `resize` (`none`): The group changed size, so panels with sizes in pixels moved to stay within their bounds.
- `imperative` (`none`): A controller or `api` call, or a property set on a web component.

### Controller

Create one with `createResizableController()` and pass it as `controller`.

Write methods take an optional `{ reason, event }`, run the change callback like a drag, and return `false` when nothing changed or the change was canceled.

- `getLayout()` (`() => number[]`): Each panel's size in percent, in order.
- `setLayout(layout, options?)` (`(layout: number[], options?: ResizableChangeOptions | undefined) => boolean`): Sets every panel's size at once, in percent. It's fitted to the panels' bounds and adds up to 100. Returns whether anything changed.
- `collapse(panel, options?)` (`(panel: string | number, options?: ResizableChangeOptions | undefined) => boolean`): Collapses a collapsible panel, by its id or its index. Returns whether it collapsed.
- `expand(panel, options?)` (`(panel: string | number, options?: ResizableChangeOptions | undefined) => boolean`): Expands a collapsed panel to the size it had before it collapsed, or its minimum. Returns whether it expanded.
- `resize(panel, size, options?)` (`(panel: string | number, size: ResizableSize, options?: ResizableChangeOptions | undefined) => boolean`): Resizes one panel, by its id or its index, like `resize("sidebar", "240px")`. Its neighbours give or take the difference.
- `isCollapsed(panel)` (`(panel: string | number) => boolean`): Whether a panel is collapsed.

### Events

Callbacks get the new layout first, then the details. A bound `layout` only updates when `onLayoutChange` wasn't canceled.

| Prop             | On          | Receives                                                       |
| ---------------- | ----------- | -------------------------------------------------------------- |
| `bind:layout`    | `Resizable` | The layout: one percentage per panel                           |
| `onLayoutChange` | `Resizable` | `(layout, details)`: the new layout and the [reason](#reasons) |
| `onLayoutCommit` | `Resizable` | `(layout, details)`: the layout once the interaction ends      |

## Accessibility

Handles follow the [WAI-ARIA window splitter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/). Each is a focusable `separator` whose `aria-valuenow` is the size of the panel before it, with `aria-valuemin` and `aria-valuemax` as far as the panels allow, `aria-controls` pointing at that panel, and `aria-orientation` running across the group. Name each handle with `aria-label`.

### Keyboard

Focus a handle and press the arrow keys along the group's axis to move it by `step`, or with Shift by `largeStep`. Home and End move it as far as the panels allow, and Enter collapses or restores a collapsible panel beside it. A panel at its minimum passes the rest of a move on to the next one. Disabled handles leave the tab order.

### Tested Behavior

Every line below is an automated test.

**Keyboard**

- Starts at each panel's default size, and panels without one share the rest.
- ArrowRight grows the panel before the handle by one step, and ArrowLeft shrinks it.
- Shift with an arrow key moves by the large step.
- Arrows across the group's axis do nothing.
- ArrowDown and ArrowUp move a vertical group's handle.
- Home collapses a collapsible panel, and End grows it to its maximum.
- Home stops at the minimum of a panel that can't collapse.
- Enter collapses the panel before the handle, and restores its size.
- Enter collapses the panel after the handle when only that one can collapse.
- One step past a collapsible panel's minimum collapses it.
- A panel at its minimum passes the rest on to the next one.
- End stops where the panels' bounds meet.
- Keys pressed without waiting all land.
- Keys complete the change once they come up.
- Step and large step set how far keys move.
- A canceled change keeps the layout.
- A controlled group follows its layout.
- A controlled group that ignores a change completes nothing. React and Vue only.
- A disabled handle stops a move from the next one, so the panels past it keep their sizes.
- DefaultLayout sets where the panels start.
- A disabled group's handles leave the tab order and ignore drags.

**Pointer**

- Dragging a handle moves it with the pointer.
- A drag reports its moves with the drag reason and completes once.
- Dragging past a disabled handle moves nothing beyond it.
- Dragging a vertical group's handle moves it up and down.
- Dragging past half a collapsible panel's minimum collapses it, and back opens it.
- A drag stops at the panels' bounds.
- A double-click on a handle collapses its panel, and another restores it.
- A press just beside the thin handle drags it.
- A press farther from the handle doesn't drag.
- Where two handles meet, one drag moves both.
- A drag shows the axis cursor over the whole page until it ends.

**Controller**

- Collapses and expands a panel, with the imperative reason.
- Resizes a panel to a size in pixels.

**Container**

- A panel with a minimum in pixels keeps it as the group shrinks.
- Panels keep their share as the group grows.

**Pages around a group**

- A layer above a handle keeps its presses.
- A press beside a handle doesn't click what's under it.
- Only the main button drags.
- An iframe under the pointer doesn't stop a drag.
- At a limit, the cursor points the only way a handle can go.
- A saved layout is there on the first paint.

Runs in React, Vue and web components.
