@mshafiqyajid/react-split
Headless split hook and styled component. Two-or-more pane resizable panel — horizontal or vertical, drag to resize, double-click any resizer to equalize all panes, full keyboard support (arrow keys, Home, End), ARIA separator role, controlled and uncontrolled modes, per-pane min/max constraints, collapsible panes with toggle button, snap points, persistent sizes via localStorage, smooth transition only when not dragging, reduced-motion support. Zero dependencies.
Playground #
Drag the divider or focus it and use arrow keys (Shift for 5% jumps), Home, and End. Double-click a divider to equalize all panes.
import { SplitStyled } from "@mshafiqyajid/react-split/styled";
import "@mshafiqyajid/react-split/styles.css";
<SplitStyled />Install #
npm install @mshafiqyajid/react-split Quick start #
import { SplitStyled } from "@mshafiqyajid/react-split/styled";
import "@mshafiqyajid/react-split/styles.css";
<SplitStyled style={{ height: 400 }}>
<div>Left pane content</div>
<div>Right pane content</div>
</SplitStyled> Orientation #
Set orientation to "horizontal" (default, left/right panes) or "vertical" (top/bottom panes).
{/* Horizontal — left and right panes (default) */}
<SplitStyled orientation="horizontal" style={{ height: 300 }}>
<div>Left</div>
<div>Right</div>
</SplitStyled>
{/* Vertical — top and bottom panes */}
<SplitStyled orientation="vertical" style={{ height: 400 }}>
<div>Top</div>
<div>Bottom</div>
</SplitStyled> 3-pane layout #
Pass 3 or more children to get a multi-pane split with N−1 independent resizers. Use defaultSizes as an array of N percentages summing to 100. Double-click any resizer to equalize all panes.
<SplitStyled defaultSizes={[25, 50, 25]} style={{ height: 400 }}>
<aside>Sidebar</aside>
<main>Content</main>
<aside>Inspector</aside>
</SplitStyled>
{/* 4-pane example */}
<SplitStyled defaultSizes={[20, 30, 30, 20]} style={{ height: 400 }}>
<div>Pane 1</div>
<div>Pane 2</div>
<div>Pane 3</div>
<div>Pane 4</div>
</SplitStyled> Double-click to equalize #
Double-click any resizer to snap all panes to equal sizes (e.g. 50/50 for 2 panes, 33/33/34 for 3 panes). A native tooltip "Double-click to equalize" appears on hover. This works with the headless hook too via getResizerProps(index).onDoubleClick.
Controlled sizes #
Pass sizes and onResize to control the panel sizes from state. Sizes are percentages that must sum to 100.
const [sizes, setSizes] = useState<number[]>([40, 60]);
<SplitStyled
sizes={sizes}
onResize={setSizes}
onResizeEnd={(s) => console.log("resize ended", s)}
style={{ height: 300 }}
>
<div>Left · {sizes[0]}%</div>
<div>Right · {sizes[1]}%</div>
</SplitStyled> Min / max constraints #
Use minSize and maxSize to prevent panes from becoming too small or too large. Pass a single number for all panes, or an array for per-pane constraints.
{/* All panes: min 20%, max 80% */}
<SplitStyled minSize={20} maxSize={80} style={{ height: 300 }}>
<div>Left</div>
<div>Right</div>
</SplitStyled>
{/* Per-pane: left min 30%, right min 15% */}
<SplitStyled minSize={[30, 15]} style={{ height: 300 }}>
<div>Left</div>
<div>Right</div>
</SplitStyled> Custom resizer size #
Control the thickness of the drag handle with resizerSize (pixels).
<SplitStyled resizerSize={12} style={{ height: 300 }}>
<div>Left</div>
<div>Right</div>
</SplitStyled> Disabled #
Set disabled to prevent resizing. The resizer becomes non-interactive and the container gets data-disabled.
<SplitStyled disabled defaultSizes={[35, 65]} style={{ height: 300 }}>
<div>Fixed left</div>
<div>Fixed right</div>
</SplitStyled> Collapsible panes #
Add collapsible to show a chevron toggle button in the resizer. Clicking it collapses the adjacent pane to zero width/height; clicking again restores the previous size.
Pass a boolean to make all panes collapsible, or an array to control each pane independently. Use defaultCollapsed to set the initial state, or collapsed + onCollapseChange for controlled mode.
{/* Both panes collapsible */}
<SplitStyled collapsible style={{ height: 300 }}>
<div>Left</div>
<div>Right</div>
</SplitStyled>
{/* Only pane 0 (left/top) is collapsible */}
<SplitStyled collapsible={[true, false]} style={{ height: 300 }}>
<div>Collapsible left</div>
<div>Always visible right</div>
</SplitStyled>
{/* Start with pane 0 collapsed */}
<SplitStyled collapsible defaultCollapsed={[true, false]} style={{ height: 300 }}>
<div>Left (starts collapsed)</div>
<div>Right</div>
</SplitStyled>
{/* Controlled collapse */}
const [collapsed, setCollapsed] = useState<boolean[]>([false, false]);
<SplitStyled
collapsible
collapsed={collapsed}
onCollapseChange={setCollapsed}
style={{ height: 300 }}
>
<div>Left</div>
<div>Right</div>
</SplitStyled> The headless hook also exposes collapsed and collapse(index, value?):
const { collapsed, collapse, ...rest } = useSplit({ collapsible: true });
// Collapse pane 0
collapse(0);
// Expand pane 1
collapse(1, false); Snap points #
Pass snapPoints as an array of percentages. While dragging, if the first pane size comes within 3% of any snap point, it snaps smoothly to that value.
{/* Snap to 25%, 50%, and 75% */}
<SplitStyled snapPoints={[25, 50, 75]} style={{ height: 300 }}>
<div>Left</div>
<div>Right</div>
</SplitStyled>
{/* Third-split snap points */}
<SplitStyled snapPoints={[33, 66]} minSize={5} maxSize={95} style={{ height: 300 }}>
<div>Left</div>
<div>Right</div>
</SplitStyled> Persistent sizes #
Set persistent to a unique string key. The pane sizes are saved in localStorage under rspl:<key> and restored on mount. This is SSR-safe — storage access is guarded with typeof window !== "undefined".
{/* Size is saved/restored across page loads */}
<SplitStyled persistent="my-editor-layout" style={{ height: 300 }}>
<div>File tree</div>
<div>Editor</div>
</SplitStyled>
{/* Multiple independent splits on one page */}
<SplitStyled persistent="sidebar" style={{ height: 300 }}>
<div>Sidebar</div>
<div>Content</div>
</SplitStyled>
<SplitStyled persistent="console" orientation="vertical" style={{ height: 300 }}>
<div>Editor</div>
<div>Console</div>
</SplitStyled> Keyboard navigation #
Focus the resizer (Tab to reach it) and use:
- ArrowLeft / ArrowRight — move 1% (horizontal)
- ArrowUp / ArrowDown — move 1% (vertical)
- Shift + arrow — move 5%
- Home — snap to minimum
- End — snap to maximum
Headless #
Use useSplit directly for full control over markup and styling. getResizerProps(index) accepts a resizer index for multi-pane layouts.
import { useSplit } from "@mshafiqyajid/react-split";
function MySplit() {
const {
containerProps,
getPaneProps,
getResizerProps,
sizes,
isDragging,
collapsed,
collapse,
} = useSplit({
orientation: "horizontal",
defaultSizes: [25, 50, 25],
minSize: 10,
maxSize: 90,
collapsible: true,
snapPoints: [25, 50, 75],
persistent: "my-split",
});
return (
<div {...containerProps} style={{ ...containerProps.style, height: 300 }}>
<div {...getPaneProps(0)} className="my-pane">
Left · {Math.round(sizes[0])}%
</div>
<div {...getResizerProps(0)} className="my-resizer" />
<div {...getPaneProps(1)} className="my-pane">
Center · {Math.round(sizes[1])}%
</div>
<div {...getResizerProps(1)} className="my-resizer" />
<div {...getPaneProps(2)} className="my-pane">
Right · {Math.round(sizes[2])}%
</div>
</div>
);
} API #
| Prop | Type | Default | Description |
|---|---|---|---|
| orientation | "horizontal" | "vertical" | "horizontal" | Direction of the split — horizontal = left/right, vertical = top/bottom |
| defaultSizes | number[] | equal split | Initial pane sizes as percentages (must sum to 100); uncontrolled only |
| sizes | number[] | — | Controlled pane sizes as percentages |
| onResize | (sizes: number[]) => void | — | Called continuously while dragging or on each keyboard step |
| onResizeEnd | (sizes: number[]) => void | — | Called once when drag ends, keyboard key is released, or panes are equalized |
| minSize | number | number[] | 10 | Minimum pane size in percent — single value for all panes, or per-pane array |
| maxSize | number | number[] | 90 | Maximum pane size in percent — single value for all panes, or per-pane array |
| resizerSize | number | 6 | Thickness of the drag divider in pixels |
| disabled | boolean | false | Disable resizing; adds data-disabled to root |
| collapsible | boolean | boolean[] | — | Show collapse toggle button in resizer — true for all panes, or per-pane array |
| collapsed | boolean[] | — | Controlled collapsed state for each pane |
| defaultCollapsed | boolean[] | [false, ...] | Initial collapsed state; uncontrolled only |
| onCollapseChange | (collapsed: boolean[]) => void | — | Called when any pane is collapsed or expanded |
| snapPoints | number[] | — | Snap pane 0 to these percentages when dragging within 3% of each point |
| persistent | string | — | localStorage key suffix — saves/restores sizes under rspl:<key> |
| children | ReactNode[] | — | Two or more children — one per pane |
| className | string | — | Extra class on root element |
| style | CSSProperties | — | Inline style override on root element (use to set height) |
| ref | Ref<HTMLDivElement> | — | Forwarded ref to the container div |
Data attributes #
| Attribute | Values | Where | Description |
|---|---|---|---|
| data-orientation | "horizontal" | "vertical" | Root | Current orientation |
| data-dragging | "true" | absent | Root | Present while user is dragging; use to suppress pane transitions |
| data-disabled | present | absent | Root | Present when disabled is true |
| data-collapsed-N | "true" | absent | Root | Present when pane N is collapsed (e.g. data-collapsed-0, data-collapsed-1) |
| data-collapsed | "true" | absent | Pane | Present on a pane when it is currently collapsed |
CSS variables #
:root {
--rspl-resizer-bg: #e4e4e7; /* resizer track color */
--rspl-resizer-bg-hover: #d4d4d8; /* resizer track on hover */
--rspl-resizer-bg-active: #a1a1aa; /* resizer track while dragging */
--rspl-resizer-handle-bg: #a1a1aa; /* center handle line color */
--rspl-resizer-handle-bg-hover: #71717a; /* center handle on hover */
--rspl-pane-transition: flex-basis 150ms ease; /* pane size transition */
--rspl-focus-ring: 0 0 0 2px #6366f1; /* resizer focus ring */
}