Resizable
Split a layout into draggable, resizable panels — horizontal or vertical, with per-panel min and max sizes.
Handles are keyboard-operable separators (role="separator", aria-orientation, arrow keys to resize, Home/End to clamp).
Preview
tsx
import { Resizable, ResizableHandle, ResizablePanel } from '@arshad-shah/cynosure-react';
const paneStyle = {
height: '100%',
display: 'grid',
placeItems: 'center',
padding: '1rem',
background: 'var(--cynosure-color-surface-2)',
};
export default function Example() {
return (
<div
style={{
height: 240,
width: '100%',
border: '1px solid var(--cynosure-color-border)',
borderRadius: 'var(--cynosure-radius-md)',
overflow: 'hidden',
}}
>
<Resizable direction="horizontal">
<ResizablePanel defaultSize={40}>
<div style={paneStyle}>Left</div>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={60}>
<div style={paneStyle}>Right</div>
</ResizablePanel>
</Resizable>
</div>
);
}
Vertical split
Section titled “Vertical split”Pass direction="vertical" for a top/bottom split — the handle becomes a horizontal bar.
Preview
tsx
import { Resizable, ResizableHandle, ResizablePanel } from '@arshad-shah/cynosure-react';
const paneStyle = {
height: '100%',
display: 'grid',
placeItems: 'center',
padding: '1rem',
background: 'var(--cynosure-color-surface-2)',
};
export default function Example() {
return (
<div
style={{
height: 280,
width: '100%',
border: '1px solid var(--cynosure-color-border)',
borderRadius: 'var(--cynosure-radius-md)',
overflow: 'hidden',
}}
>
<Resizable direction="vertical">
<ResizablePanel defaultSize={40}>
<div style={paneStyle}>Top</div>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={60}>
<div style={paneStyle}>Bottom</div>
</ResizablePanel>
</Resizable>
</div>
);
}
Min / max size
Section titled “Min / max size”Each ResizablePanel accepts minSize and maxSize (in percent) to clamp how far the surrounding handles can drag.
Preview
tsx
import { Resizable, ResizableHandle, ResizablePanel } from '@arshad-shah/cynosure-react';
const paneStyle = {
height: '100%',
display: 'grid',
placeItems: 'center',
padding: '1rem',
background: 'var(--cynosure-color-surface-2)',
};
export default function Example() {
return (
<div
style={{
height: 240,
width: '100%',
border: '1px solid var(--cynosure-color-border)',
borderRadius: 'var(--cynosure-radius-md)',
overflow: 'hidden',
}}
>
<Resizable direction="horizontal">
<ResizablePanel defaultSize={25} minSize={15} maxSize={40}>
<div style={paneStyle}>Sidebar (15–40%)</div>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={75}>
<div style={paneStyle}>Main</div>
</ResizablePanel>
</Resizable>
</div>
);
}
Nested layouts
Section titled “Nested layouts”Compose horizontal and vertical groups for IDE-style layouts (sidebar + editor + preview).
Preview
tsx
import { Resizable, ResizableHandle, ResizablePanel } from '@arshad-shah/cynosure-react';
const paneStyle = {
height: '100%',
width: '100%',
display: 'grid',
placeItems: 'center',
padding: '1rem',
background: 'var(--cynosure-color-surface-2)',
};
export default function Example() {
return (
<div
style={{
height: 320,
width: '100%',
border: '1px solid var(--cynosure-color-border)',
borderRadius: 'var(--cynosure-radius-md)',
overflow: 'hidden',
}}
>
<Resizable direction="horizontal">
<ResizablePanel defaultSize={25}>
<div style={paneStyle}>Files</div>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={75}>
<Resizable direction="vertical">
<ResizablePanel defaultSize={65}>
<div style={paneStyle}>Editor</div>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={35}>
<div style={paneStyle}>Terminal</div>
</ResizablePanel>
</Resizable>
</ResizablePanel>
</Resizable>
</div>
);
}
PropTypeDefaultDescription
direction
"horizontal"|"vertical"
horizontal
Layout axis. `horizontal` arranges panels side-by-side; `vertical` stacks
them top-to-bottom.
defaultLayout
Layout
—
Default layout for the Group.
ℹ️ This value allows layouts to be remembered between page reloads.
⚠️ Slight layout shift may occur when server-rendering panels with percentage-based default sizes.
Refer to the documentation for suggestions on how to minimize the impact of this.
disableCursor
boolean
—
This library sets custom mouse cursor styles to indicate drag state.
Use this prop to disable that behavior for Panels and Separators in this group.
disabled
boolean
—
Disable resize functionality.
elementRef
Ref<HTMLDivElement | null>
—
Ref attached to the root `HTMLDivElement`.
groupRef
Ref<GroupImperativeHandle | null>
—
Exposes the following imperative API:
- `getLayout(): Layout`
- `setLayout(layout: Layout): void`
ℹ️ The `useGroupRef` and `useGroupCallbackRef` hooks are exported for convenience use in TypeScript projects.
onLayoutChange
((layout: Layout) => void)
—
Called when the Group's layout is changing.
⚠️ For layout changes caused by pointer events, this method is called each time the pointer is moved.
For most cases, it is recommended to use the `onLayoutChanged` callback instead.
onLayoutChanged
((layout: Layout) => void)
—
Called after the Group's layout has been changed.
ℹ️ For layout changes caused by pointer events, this method is not called until the pointer has been released.
This method is recommended when saving layouts to some storage api.
resizeTargetMinimumSize
{ coarse: number; fine: number; }
—
Minimum size of the resizable hit target area (either `Separator` or `Panel` edge)
This threshold ensures are large enough to avoid mis-clicks.
- Coarse inputs (typically a finger on a touchscreen) have reduced accuracy;
to ensure accessibility and ease of use, hit targets should be larger to prevent mis-clicks.
- Fine inputs (typically a mouse) can be smaller
ℹ️ [Apple interface guidelines](https://developer.apple.com/design/human-interface-guidelines/accessibility) suggest `20pt` (`27px`) on desktops and `28pt` (`37px`) for touch devices
In practice this seems to be much larger than many of their own applications use though.
Accessibility
Section titled “Accessibility”- Each handle is a
role="separator"element with the correctaria-orientationfor the group direction. - Arrow keys resize the surrounding panels;
Home/Endclamp to the min / max bounds. - Pointer and keyboard drags both honour each panel’s
minSizeandmaxSize. - The group itself is a
<div>— wrap it in a<section>(or passaria-label) when its purpose is ambiguous. - Pass children to
ResizableHandleto render a custom grip while keeping the separator semantics.
Recipes
Section titled “Recipes”- Nest a vertical
Resizableinside a horizontal one for IDE-style layouts (sidebar + editor + preview). - Pass
withHandleto show the grip — a thick rounded line centered on the divider that grows and turns the accent colour while you drag. The divider has an enlarged invisible grab zone, so it’s easy to grab anywhere along the border. - Set
defaultSize(percent) on each panel so the initial split is predictable across reloads. - Pair
minSizewith a sensibledefaultSizeso a panel never collapses below readability.