react ~3 KB 0 deps v0.1.0 ↗ GitHub ↗

@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 #

Panel A · 50%
Panel B · 50%

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.

Props
TSX
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 #

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"Direction of the split — horizontal = left/right, vertical = top/bottom
defaultSizesnumber[]equal splitInitial pane sizes as percentages (must sum to 100); uncontrolled only
sizesnumber[]Controlled pane sizes as percentages
onResize(sizes: number[]) => voidCalled continuously while dragging or on each keyboard step
onResizeEnd(sizes: number[]) => voidCalled once when drag ends, keyboard key is released, or panes are equalized
minSizenumber | number[]10Minimum pane size in percent — single value for all panes, or per-pane array
maxSizenumber | number[]90Maximum pane size in percent — single value for all panes, or per-pane array
resizerSizenumber6Thickness of the drag divider in pixels
disabledbooleanfalseDisable resizing; adds data-disabled to root
collapsibleboolean | boolean[]Show collapse toggle button in resizer — true for all panes, or per-pane array
collapsedboolean[]Controlled collapsed state for each pane
defaultCollapsedboolean[][false, ...]Initial collapsed state; uncontrolled only
onCollapseChange(collapsed: boolean[]) => voidCalled when any pane is collapsed or expanded
snapPointsnumber[]Snap pane 0 to these percentages when dragging within 3% of each point
persistentstringlocalStorage key suffix — saves/restores sizes under rspl:<key>
childrenReactNode[]Two or more children — one per pane
classNamestringExtra class on root element
styleCSSPropertiesInline style override on root element (use to set height)
refRef<HTMLDivElement>Forwarded ref to the container div

Data attributes #

AttributeValuesWhereDescription
data-orientation"horizontal" | "vertical"RootCurrent orientation
data-dragging"true" | absentRootPresent while user is dragging; use to suppress pane transitions
data-disabledpresent | absentRootPresent when disabled is true
data-collapsed-N"true" | absentRootPresent when pane N is collapsed (e.g. data-collapsed-0, data-collapsed-1)
data-collapsed"true" | absentPanePresent 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 */
}
Edit this page on GitHub