One group owns the sizes of its panels. Panels and handles must be direct children in order, and every pointer, keyboard and programmatic change goes through the same constraint solver. Give the group a block size; panels share its space along orientation. Nest another group inside a panel for the second axis, as above.
The resize handle is independent of the decorative tp-separator component. Ordinary direct-child elements also serve as panels; omitted handles are generated between adjacent panels and use the same implementation.
Sizes and limits
default-size, min-size and max-size take percentages, pixels or other lengths; a unitless value is a percentage. Double-clicking a handle restores its panel’s default size.
The target selects the panel described by accessible values, keyboard commands and reset. Pointer dragging follows the physical boundary. An enabled handle is a focusable separator with native orientation, values and a relationship to the controlled panel. Its separator orientation is perpendicular to the group axis.
Axis arrows adjust by keyboardStep; Home/End reach allowed extremes; Enter toggles a collapsible target. F6 and Shift+F6 cycle enabled handles.
Bounds
Default density
Compact density
Restart from the initial state
Show the copyable source
A collapsible panel snaps to collapsed-size when dragged below its minimum, and Enter on its handle toggles it. collapse(), expand() and isCollapsed() drive it from your own controls.
Expansion restores the most recent valid expanded size, then the default or minimum.
Collapsible sidebar
Default density
Compact density
Restart from the initial state
Show the copyable source
tp-layout-change reports each committed layout as panel IDs and percentages, and tp-layout-change-complete fires once when a drag settles. Save layouts from the settled event, keyed by stable panel IDs.
Stable authored panel IDs are required when saving a layout across page loads. Missing IDs are generated for the current document lifetime.
Layout events
Default density
Compact density
Restart from the initial state
Show the copyable source
Set sizes to own the layout, in panel order, and accept each proposal in onSizesChange by assigning event.detail.value back; ignoring it keeps the previous layout. Strings are percentages and numbers are pixels. Like the persistence adapter, sizes must be set before the group connects.
Layout presets
Default density
Compact density
Restart from the initial state
Show the copyable source
Side by sideFocus on code
checkout.tsx
Preview
Side by sideFocus on code
checkout.tsx
Preview
<div style="display:grid;gap:var(--tp-space-3)">
<div style="display:flex;gap:var(--tp-space-2)">
<tp-button id="resizable-preset-split" variant="outline" size="sm">Side by side</tp-button>
<tp-button id="resizable-preset-focus" variant="outline" size="sm">Focus on code</tp-button>
</div>
<tp-resizable-panel-group id="resizable-controlled" style="block-size:10rem;border:var(--tp-border-width) solid var(--tp-border);border-radius:var(--tp-radius-lg)">
<tp-resizable-panel id="resizable-controlled-code" min-size="20%">
<div style="padding:var(--tp-space-4);font-size:var(--tp-text-sm)">checkout.tsx</div>
</tp-resizable-panel>
<tp-resizable-handle label="Resize code" with-handle></tp-resizable-handle>
<tp-resizable-panel id="resizable-controlled-preview" min-size="15%">
<div style="padding:var(--tp-space-4);font-size:var(--tp-text-sm)">Preview</div>
</tp-resizable-panel>
</tp-resizable-panel-group>
</div>
<script type="module">
// sizes must be set before the group first connects, so this demo swaps in a fresh copy.
await customElements.whenDefined('tp-resizable-panel-group');
const authored = document.querySelector('#resizable-controlled');
const group = authored.cloneNode(true);
group.sizes = ['50%', '50%'];
group.onSizesChange = (event) => {
if (!event.defaultPrevented) group.sizes = event.detail.value;
};
authored.replaceWith(group);
document.querySelector('#resizable-preset-split').addEventListener('click', () => {
group.sizes = ['50%', '50%'];
});
document.querySelector('#resizable-preset-focus').addEventListener('click', () => {
group.sizes = ['80%', '20%'];
});
</script>
Saved layout
Give the group a persistence-key and a persistenceAdapter with load(key) and save(key, layout), and it restores the saved layout on connect and saves each settled one. The library has no storage policy of its own, and the adapter must be in place before the group connects.
Persisted sizes
Default density
Compact density
Restart from the initial state
Show the copyable source
Folders
Messages
Drag the handle, then press Refresh or reload the page.
Folders
Messages
Drag the handle, then press Refresh or reload the page.
<div style="display:grid;gap:var(--tp-space-3)">
<tp-resizable-panel-group id="resizable-saved" persistence-key="docs-mail-layout" style="block-size:10rem;border:var(--tp-border-width) solid var(--tp-border);border-radius:var(--tp-radius-lg)">
<tp-resizable-panel id="resizable-saved-folders" default-size="25%" min-size="15%">
<div style="padding:var(--tp-space-4);font-size:var(--tp-text-sm)">Folders</div>
</tp-resizable-panel>
<tp-resizable-handle label="Resize folders" with-handle></tp-resizable-handle>
<tp-resizable-panel id="resizable-saved-messages" min-size="30%">
<div style="padding:var(--tp-space-4);font-size:var(--tp-text-sm)">Messages</div>
</tp-resizable-panel>
</tp-resizable-panel-group>
<span style="color:var(--tp-muted-foreground);font-size:var(--tp-text-sm)">Drag the handle, then press Refresh or reload the page.</span>
</div>
<script type="module">
// The adapter must be set before the group first connects, so this demo swaps in a fresh copy.
await customElements.whenDefined('tp-resizable-panel-group');
const authored = document.querySelector('#resizable-saved');
const group = authored.cloneNode(true);
group.persistenceAdapter = {
load: (key) => {
try {
return JSON.parse(localStorage.getItem(key) ?? 'null');
} catch {
return null;
}
},
save: (key, layout) => {
try {
localStorage.setItem(key, JSON.stringify(layout));
} catch {}
},
};
authored.replaceWith(group);
</script>
Disabled
disabled on the group freezes every handle; on a panel, it keeps that panel’s pixel size while the others adjust.
Disabled
Default density
Compact density
Restart from the initial state
Show the copyable source
ResizeSeparatorController is the Handle’s separator behavior for one extent that its owner controls outside a group, such as a table column. The owner supplies the current value, its limits and the key steps, and applies the proposals it receives:
The arrow pointing to the end of the axis grows the extent and the opposite arrow shrinks it, mirrored in right-to-left content; Shift and Alt use their own steps, and Home and End reach the limits. A drag captures the pointer, blocks text selection and shows the resize cursor; Escape and pointer cancellation restore the start value. resizeSeparatorTargetCss(selector, axis) returns the structural rule of the enlarged pointer target, which reads --tp-resize-target-fine and --tp-resize-target-coarse with 10 and 20 pixel fallbacks.
API Reference
Every element also accepts motion-policy.
Attributes
AttributePropertyTypeDefaultDescriptionminminnumber | undefined—A minimum percentage for every panel that does not declare its own min-size.keyboard-stepkeyboardStepnumber1How far, in pixels, an arrow key moves a handle.with-handlewithHandlebooleanfalseShows a grip on the generated handles; authored handles set their own.disable-cursordisableCursorbooleanfalseLeaves the document cursor alone while dragging, instead of showing the resize cursor everywhere.disable-double-click-resetdisableDoubleClickResetbooleanfalseStops a double-click on a handle from restoring its panel's default size.persistence-keypersistenceKeystring''The key the persistence adapter saves and restores the layout under.
Properties
PropertyTypeDefaultDescriptionsizesreadonly PanelExtent[]—The panel sizes in panel order; set it to control the layout, then publish each accepted proposal back. Numbers are pixels, unitless strings percentages.defaultSizesreadonly PanelExtent[] | undefined—The initial sizes in panel order, when the group owns its layout.defaultLayoutPanelLayout | undefined—The initial layout by panel id, as percentages, when the group owns its layout; survives reordering.resizeTargetMinimumSizeobject{ fine: 10, coarse: 20 }The smallest pointer target of each handle, in pixels, for fine and coarse pointers; the visible line stays thin.persistenceAdapterPanelPersistenceAdapter | undefined—Your storage for the layout: load(key) restores it before the first commit, save(key, layout) stores each settled layout. Controlled groups never use it.onSizesChange((event: TpValueChangeEvent<readonly number[]>) => void) | undefined—Called with each size proposal, as tp-value-change is dispatched.onLayoutChange((layout: PanelLayout) => void) | undefined—Called with the layout after each commit, as tp-layout-change is dispatched.onLayoutChanged((layout: PanelLayout) => void) | undefined—Called with the final layout once a resize settles, as tp-layout-change-complete is dispatched.
Events
EventTypeDescriptiontp-value-changeTpValueChangeEvent<readonly number[]>Cancelable proposal of new panel sizes, in pixels and panel order, before it commits; a controlled group must publish the accepted sizes.tp-layout-changeCustomEvent<{ layout: Readonly<Record<string, number>>; sizes: number[]; }>After each committed layout, with layout (panel id to percentage) and sizes (pixels).tp-layout-change-completeCustomEvent<{ layout: Readonly<Record<string, number>>; sizes: number[]; }>Once a resize settles, with the final layout and sizes; the place to save a layout.
Methods
MethodReturnsDescriptiongetLayout()PanelLayoutThe current layout: each panel id with its size as a percentage.setLayout(layout)PanelLayoutProposes a layout by panel id, in percentages, and returns the layout actually committed after the bounds apply.getPanelSize(panel)PanelSizeA panel's current size, in pixels and as a percentage.getPanelElement(id)HTMLElement | undefinedThe panel with this id, if the group holds one.collapsePanel(panel)voidCollapses a collapsible panel to its collapsed size.expandPanel(panel)voidExpands a collapsed panel to its last expanded size, else its default or minimum.resizePanel(panel, size)voidResizes one panel within its bounds; its neighbors absorb the difference.
Slots
SlotDescriptiondefaultPanels and handles, as direct children in their logical order.
CSS parts
PartDescription::part(resizable-panel-group)The flex container laying the panels out along the group axis.::part(resizable-panel-group-panel)A panel; marked while collapsed.
<tp-resizable-panel>
Attributes
AttributePropertyTypeDefaultDescriptiondefault-sizedefaultSizePanelExtent | undefined—The size the panel starts at and returns to on a double-click of its handle; without it, the panel shares the remaining space.min-sizeminSizePanelExtent | undefined—The smallest size a resize leaves the panel at, unless it collapses.max-sizemaxSizePanelExtent | undefined—The largest size a resize gives the panel.collapsed-sizecollapsedSizePanelExtent0The size of the panel while collapsed, between zero and its minimum.collapsiblecollapsiblebooleanfalseLets a resize past the minimum snap the panel to its collapsed size.resize-behaviorresizeBehavior'relative' | 'preserve-pixel-size''relative'How the panel answers a change in the group's size: keep its share (relative) or keep its pixel size, as a fixed sidebar does. At least one panel keeps its share.
Properties
PropertyTypeDefaultDescriptiononResize((size: PanelSize, id: string, previous: PanelSize | undefined) => void) | undefined—Called after the panel's committed size changes, as tp-panel-resize is dispatched.ownerGroupread-onlyTpResizablePanelGroup | null—The group the panel belongs to: its parent, when that is a Resizable panel group.
Events
EventTypeDescriptiontp-resizable-member-changeEventBubbles when the panel's configuration changes, so its group lays out again.tp-panel-resizeCustomEvent<{ size: { inPixels: number; asPercentage: number; }; previousSize: { inPixels: number; asPercentage: number; } | undefined; id: string; }>After the panel's committed size changes, with size and previousSize (each in pixels and as a percentage) and the panel id.
Methods
MethodReturnsDescriptioncollapse()voidCollapses the panel, when it is collapsible.expand()voidExpands the collapsed panel to its last expanded size, else its default or minimum.resize(size)voidResizes the panel within its bounds; its neighbors absorb the difference.getSize()PanelSizeThe panel's current size, in pixels and as a percentage of the group.isCollapsed()booleanWhether the panel is at its collapsed size.
Slots
SlotDescriptiondefaultThe panel content.
CSS parts
PartDescription::part(resizable-panel-group-panel)A panel; marked while collapsed.
<tp-resizable-handle>
Attributes
AttributePropertyTypeDefaultDescriptionwith-handlewithHandlebooleanfalseShows a grip on the line, a visible cue that it can be dragged.targettargetstring''The id of the adjacent panel the handle describes and adjusts with the keyboard; the preceding panel by default.labellabelstring'Resize panels'The accessible name of the separator, such as "Resize sidebar".disable-double-click-resetdisableDoubleClickResetbooleanfalseStops a double-click on this handle from restoring its panel's default size.
Properties
PropertyTypeDefaultDescriptionownerGroupTpResizablePanelGroup | undefined—The group a generated handle belongs to, which is not its parent; set by the group.boundarynumber-1The index of the boundary between panels the handle moves; set by the group.groupread-onlyTpResizablePanelGroup | null—The group the handle belongs to.separatorElementread-onlyHTMLElement | null—The focusable separator element, for focusing the handle from code.
Events
EventTypeDescriptiontp-resizable-member-changeEventBubbles when the handle's configuration changes, so its group lays out again.
CSS parts
PartDescription::part(resizable-panel-group-handle-decoration)The grip a handle shows with with-handle.::part(resizable-panel-group-separator)The focusable separator of a handle, with its enlarged pointer target; marked while dragging or disabled.