About
Resizable is built on @sveltery/resizable, a Svelte package that takes its design from react-resizable-panels by Brian Vaughn. The layout algorithm is a port of that library under the MIT License. The package has no runtime dependencies, and you bind to its state with bind:layout and bind:this instead of calling an imperative API.
Installation
Install the dependencies
pnpm add cn @sveltery/resizableCopy the component
Copy the following files into src/lib/components/ui/resizable.
<script lang="ts">
import { Separator } from '@sveltery/resizable';
import { cn } from 'cn';
import type { ResizableHandleProps } from './types.js';
let { withHandle = false, class: classProp, ...props }: ResizableHandleProps = $props();
</script>
<Separator
data-slot="resizable-handle"
class={cn('relative flex w-px items-center justify-center bg-border ring-offset-background after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90', classProp)}
{...props}
>
{#if withHandle}
<div class="bg-border h-6 w-1 rounded-lg z-10 flex shrink-0"></div>
{/if}
</Separator><script lang="ts">
import { Panel } from '@sveltery/resizable';
import type { PanelSize, SizeInput } from '@sveltery/resizable';
import type { ResizablePanelProps } from './types.js';
let { ...props }: ResizablePanelProps = $props();
let panel: ReturnType<typeof Panel> | undefined;
export function collapse(): void { panel?.collapse(); }
export function expand(): void { panel?.expand(); }
export function resize(size: SizeInput): void { panel?.resize(size); }
export function getSize(): PanelSize | undefined { return panel?.getSize(); }
export function isCollapsed(): boolean { return panel?.isCollapsed() ?? false; }
</script>
<Panel bind:this={panel} data-slot="resizable-panel" {...props} /><script lang="ts">
import { Group } from '@sveltery/resizable';
import { cn } from 'cn';
import type { ResizablePanelGroupProps } from './types.js';
let { layout = $bindable(), class: classProp, ...props }: ResizablePanelGroupProps = $props();
</script>
<Group data-slot="resizable-panel-group" bind:layout class={cn('flex h-full w-full aria-[orientation=vertical]:flex-col', classProp)} {...props} />import type { GroupProps, PanelProps, SeparatorProps } from '@sveltery/resizable';
export type ResizablePanelGroupProps = GroupProps;
export type ResizablePanelProps = PanelProps;
export type ResizableHandleProps = Omit<SeparatorProps, 'children'> & { withHandle?: boolean };
export type { Layout, LayoutChangeDetails, Orientation, PanelSize, SizeInput } from '@sveltery/resizable';export { default as ResizablePanelGroup } from './ResizablePanelGroup.svelte';
export { default as ResizablePanel } from './ResizablePanel.svelte';
export { default as ResizableHandle } from './ResizableHandle.svelte';
export type { ResizablePanelGroupProps, ResizablePanelProps, ResizableHandleProps } from './types.js';Update the import paths
Change the imports to match your project setup.
Usage
<script lang="ts">
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '$lib/components/ui/resizable';
</script><ResizablePanelGroup orientation="horizontal" class="min-h-[200px] max-w-md rounded-lg border">
<ResizablePanel defaultSize="50%">One</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize="50%">Two</ResizablePanel>
</ResizablePanelGroup>A group is as tall and wide as its parent, so give it a size, or a min-h, to see it.
Composition
Use the following composition to build a Resizable:
ResizablePanelGroup
├── ResizablePanel
├── ResizableHandle
└── ResizablePanelA handle sits between two panels and resizes the panel before it against the panel after it. Panel groups nest.
Sizes
A size is a number of pixels or a string with a unit. "25%" and "25" are percent of the group. "200px", "2rem", "1em", "50vh" and "50vw" are converted to percent from the measured group, so they follow the window. defaultSize, minSize, maxSize and collapsedSize all take a size.
<ResizablePanelGroup>
<ResizablePanel defaultSize="30%" minSize="200px" maxSize="50%">Sidebar</ResizablePanel>
<ResizableHandle />
<ResizablePanel>Content</ResizablePanel>
</ResizablePanelGroup>A panel without a defaultSize takes an equal share of what the other panels leave. The sizes of a group add up to 100 percent, and the group scales them when they do not.
Vertical
Use orientation="vertical" to stack the panels.
<ResizablePanelGroup orientation="vertical" class="min-h-[200px] max-w-md rounded-lg border">
<ResizablePanel defaultSize="25%">Header</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize="75%">Content</ResizablePanel>
</ResizablePanelGroup>Handle
Use the withHandle prop on ResizableHandle to show a visible grip.
<ResizablePanelGroup orientation="horizontal" class="min-h-[200px] max-w-md rounded-lg border">
<ResizablePanel defaultSize="25%">Sidebar</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize="75%">Content</ResizablePanel>
</ResizablePanelGroup>Controlled
Bind layout to read the sizes and to set them. The layout maps each panel id to its percent of the group, so give the panels an id to name them. A layout you assign is validated against the panel limits, and layout then holds the result.
<script lang="ts">
let layout = $state<Record<string, number>>({ sidebar: 25, content: 75 });
</script>
<ResizablePanelGroup bind:layout>
<ResizablePanel id="sidebar" defaultSize="25%">Sidebar</ResizablePanel>
<ResizableHandle />
<ResizablePanel id="content" defaultSize="75%">Content</ResizablePanel>
</ResizablePanelGroup>
<button onclick={() => (layout = { sidebar: 50, content: 50 })}>Half</button>onLayoutChange runs for every step of a drag. onLayoutChanged runs once the change settles, which is when a drag ends, and it reports whether the user caused it.
<ResizablePanelGroup
onLayoutChanged={(layout, { isUserInteraction }) => {
if (isUserInteraction) save(layout);
}}
>
…
</ResizablePanelGroup>Persistence
savedLayout keeps a layout in localStorage and restores it on the next visit. Spread its result on the group. Pass storage to use another store, such as sessionStorage. A missing or blocked storage leaves the group on its default sizes.
<script lang="ts">
import { savedLayout } from '@sveltery/resizable';
const persisted = savedLayout({ id: 'editor' });
</script>
<ResizablePanelGroup {...persisted}>
<ResizablePanel id="files" defaultSize="30%">Files</ResizablePanel>
<ResizableHandle />
<ResizablePanel id="code" defaultSize="70%">Code</ResizablePanel>
</ResizablePanelGroup>The server renders the default sizes, and the browser applies the saved ones when the page loads.
Collapsible panels
A collapsible panel snaps to collapsedSize when it is dragged below half way between collapsedSize and minSize, and snaps back to minSize when it is dragged above that. Set collapsedThreshold to choose that distance instead: with collapsedSize="5%", collapsedThreshold="5%" and minSize="25%", the panel collapses below 20% and expands above 10%. Press Enter on the handle to collapse the panel before it, and press Enter again to restore its last size. data-collapsed marks a collapsed panel for styling.
<ResizablePanelGroup>
<ResizablePanel defaultSize="30%" minSize="20%" collapsible>Sidebar</ResizablePanel>
<ResizableHandle />
<ResizablePanel>Content</ResizablePanel>
</ResizablePanelGroup>Bind the panel with bind:this to collapse, expand or resize it from your own controls.
<script lang="ts">
let sidebar = $state<ResizablePanel>();
</script>
<button onclick={() => sidebar?.collapse()}>Collapse</button>
<button onclick={() => sidebar?.expand()}>Expand</button>
<button onclick={() => sidebar?.resize('40%')}>40%</button>
<ResizablePanelGroup>
<ResizablePanel bind:this={sidebar} defaultSize="30%" minSize="20%" collapsible>Sidebar</ResizablePanel>
<ResizableHandle />
<ResizablePanel>Content</ResizablePanel>
</ResizablePanelGroup>Keyboard
A handle is a focusable window splitter. It follows the WAI-ARIA pattern and reports the size of the panel before it in aria-valuenow, aria-valuemin and aria-valuemax.
| Key | Action |
|---|---|
| Arrow Left, Arrow Right | Move a vertical handle by 5 percent in a horizontal group |
| Arrow Up, Arrow Down | Move a horizontal handle by 5 percent in a vertical group |
| Home, End | Move the handle as far as the panels allow |
| Enter | Collapse or expand the panel before the handle, when it is collapsible |
| F6, Shift+F6 | Move focus to the next or previous handle in the group |
Double-click a handle to reset the first panel beside it that has a defaultSize to that size. Set disableDoubleClick on the handle to turn this off.
RTL
A horizontal group follows the text direction. In a right-to-left page the first panel sits on the right, a drag moves the handle with the pointer, and Arrow Left and Arrow Right swap. Set dir on the group or one of its ancestors.
<div dir="rtl">
<ResizablePanelGroup orientation="horizontal">
<ResizablePanel defaultSize="50%">One</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize="50%">Two</ResizablePanel>
</ResizablePanelGroup>
</div>API reference
ResizablePanelGroup
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | 'horizontal' \| 'vertical' | 'horizontal' | The direction the panels sit in |
layout | Record<string, number> | Bindable. Percent of the group for each panel id | |
defaultLayout | Record<string, number> | The layout to start from. Panels it does not name use their defaultSize | |
onLayoutChange | (layout) => void | Runs for every change, including each step of a drag | |
onLayoutChanged | (layout, { isUserInteraction }) => void | Runs once a change settles | |
disabled | boolean | false | Turns off resizing for every handle |
class | string | Added to the group |
ResizablePanel
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | generated | Names the panel in the layout |
defaultSize | number \| string | The starting size. A number is pixels | |
minSize | number \| string | 0 | The smallest size |
maxSize | number \| string | 100% | The largest size |
collapsible | boolean | false | Lets the panel collapse to collapsedSize |
collapsedSize | number \| string | 0 | The size when collapsed |
collapsedThreshold | number \| string | half way | How far past minSize the panel moves before it collapses, and past collapsedSize before it expands |
onResize | (size, id, previous) => void | Runs when the panel size changes. A size is { asPercentage, inPixels } | |
class | string | Added to the panel |
A panel bound with bind:this has these methods.
| Method | Description |
|---|---|
collapse() | Collapses a collapsible panel |
expand() | Restores a collapsed panel to its last size, or to minSize |
resize(size) | Resizes the panel to a size |
getSize() | Returns { asPercentage, inPixels } |
isCollapsed() | Returns whether the panel is collapsed |
ResizableHandle
| Prop | Type | Default | Description |
|---|---|---|---|
withHandle | boolean | false | Shows a grip in the handle |
disabled | boolean | false | Turns off resizing for this handle |
disableDoubleClick | boolean | false | Stops a double click from resetting the panel beside the handle to its default size |
class | string | Added to the handle |
ResizablePanelGroup, ResizablePanel and ResizableHandle pass every other attribute to their div.
savedLayout
| Option | Type | Default | Description |
|---|---|---|---|
id | string | Names the saved layout. The key is sveltery-resizable:<id> | |
storage | { getItem, setItem } | localStorage | Where to keep the layout |
It returns { id, defaultLayout, onLayoutChanged } to spread on a ResizablePanelGroup.