The Split Pane widget creates resizable panels separated by draggable dividers, perfect for multi-pane layouts like editors and dashboards.
Basic Usage
import { ui } from "@rezi-ui/core";
import { defineWidget, useState } from "@rezi-ui/core";
const MySplitPane = defineWidget((ctx) => {
const [sizes, setSizes] = useState([50, 50]);
return ui.splitPane({
id: ctx.id("split"),
direction: "horizontal",
sizes,
onResize: setSizes,
}, [
ui.panel("Left Panel", [ui.text("Left content")]),
ui.panel("Right Panel", [ui.text("Right content")]),
]);
});
Props
Widget identifier. Required for interaction.
direction
horizontal | vertical
required
Split direction:
"horizontal" - Left/right panels with vertical divider
"vertical" - Top/bottom panels with horizontal divider
Panel sizes as percentages (0-100) or absolute cells, depending on sizeMode.
sizeMode
percent | absolute
default:"percent"
Size interpretation:
"percent" - Sizes are percentages (0-100)
"absolute" - Sizes are fixed cell counts
onResize
(sizes: number[]) => void
required
Callback invoked when divider is dragged. Update state with new sizes.
Minimum panel sizes (cells). Prevents panels from shrinking below these values.
Maximum panel sizes (cells). Prevents panels from growing beyond these values.
Divider width/height in cells.
Allow panels to collapse to minimum size.
Array of collapsed panel indices.
onCollapse
(index: number, collapsed: boolean) => void
Callback when panel collapse state changes.
Directions
Horizontal Split
Left and right panels with vertical divider:
ui.splitPane({
id: "hsplit",
direction: "horizontal",
sizes: [40, 60],
onResize: (sizes) => setState({ sizes }),
}, [
ui.box({ border: "single" }, [ui.text("Left")]),
ui.box({ border: "single" }, [ui.text("Right")]),
]);
Vertical Split
Top and bottom panels with horizontal divider:
ui.splitPane({
id: "vsplit",
direction: "vertical",
sizes: [50, 50],
onResize: (sizes) => setState({ sizes }),
}, [
ui.box({ border: "single" }, [ui.text("Top")]),
ui.box({ border: "single" }, [ui.text("Bottom")]),
]);
Size Modes
Percentage (Default)
Sizes are percentages of available space:
ui.splitPane({
id: "split",
direction: "horizontal",
sizes: [25, 75], // 25% / 75%
sizeMode: "percent",
onResize: (sizes) => setState({ sizes }),
}, panels);
Absolute
Sizes are fixed cell counts:
ui.splitPane({
id: "split",
direction: "horizontal",
sizes: [20, 60], // 20 cells / 60 cells
sizeMode: "absolute",
onResize: (sizes) => setState({ sizes }),
}, panels);
Size Constraints
Minimum Sizes
Prevent panels from shrinking too small:
ui.splitPane({
id: "split",
direction: "horizontal",
sizes: [50, 50],
minSizes: [20, 30], // Left ≥20 cells, Right ≥30 cells
onResize: (sizes) => setState({ sizes }),
}, panels);
Maximum Sizes
Prevent panels from growing too large:
ui.splitPane({
id: "split",
direction: "horizontal",
sizes: [50, 50],
minSizes: [10, 10],
maxSizes: [80, 80],
onResize: (sizes) => setState({ sizes }),
}, panels);
Collapsible Panels
Allow panels to collapse:
import { defineWidget, useState } from "@rezi-ui/core";
const CollapsibleSplit = defineWidget((ctx) => {
const [sizes, setSizes] = useState([30, 70]);
const [collapsed, setCollapsed] = useState<number[]>([]);
return ui.splitPane({
id: ctx.id("split"),
direction: "horizontal",
sizes,
collapsible: true,
collapsed,
onResize: setSizes,
onCollapse: (index, isCollapsed) => {
setCollapsed(isCollapsed ? [index] : []);
},
}, [
ui.panel("Sidebar", [ui.text("Sidebar content")]),
ui.panel("Main", [ui.text("Main content")]),
]);
});
Examples
Editor Layout
import { defineWidget, useState } from "@rezi-ui/core";
const EditorLayout = defineWidget((ctx) => {
const [sizes, setSizes] = useState([20, 80]);
return ui.splitPane({
id: ctx.id("editor-split"),
direction: "horizontal",
sizes,
minSizes: [15, 40],
onResize: setSizes,
}, [
// File tree
ui.box({ border: "single", p: 1 }, [
ui.text("Files", { variant: "heading" }),
ui.text("src/"),
ui.text(" index.ts"),
ui.text(" app.ts"),
]),
// Editor pane
ui.box({ border: "single", p: 1 }, [
ui.text("Editor", { variant: "heading" }),
ui.text("// Code here"),
]),
]);
});
const Dashboard = defineWidget((ctx) => {
const [sizes, setSizes] = useState([25, 75]);
const [collapsed, setCollapsed] = useState<number[]>([]);
return ui.splitPane({
id: ctx.id("dashboard-split"),
direction: "horizontal",
sizes,
collapsible: true,
collapsed,
minSizes: [15, 30],
onResize: setSizes,
onCollapse: (idx, isCollapsed) => {
setCollapsed(isCollapsed ? [idx] : []);
},
}, [
// Sidebar
ui.column({ gap: 1, p: 1 }, [
ui.text("Navigation", { variant: "heading" }),
ui.button({ id: "nav-1", label: "Overview" }),
ui.button({ id: "nav-2", label: "Metrics" }),
ui.button({ id: "nav-3", label: "Logs" }),
]),
// Main content
ui.column({ gap: 2, p: 1 }, [
ui.text("Dashboard", { variant: "heading" }),
ui.text("Main content area"),
]),
]);
});
Log Viewer
const LogViewer = defineWidget((ctx) => {
const [sizes, setSizes] = useState([70, 30]);
return ui.splitPane({
id: ctx.id("log-split"),
direction: "vertical",
sizes,
minSizes: [20, 10],
onResize: setSizes,
}, [
// Log output
ui.box({ border: "single", overflow: "scroll", scrollY: 0 }, [
ui.text("[INFO] Application started"),
ui.text("[DEBUG] Loading configuration"),
ui.text("[INFO] Server listening on port 3000"),
]),
// Details panel
ui.box({ border: "single", p: 1 }, [
ui.text("Details", { variant: "heading" }),
ui.text("Selected log entry details appear here"),
]),
]);
});
Nested Split Panes
const NestedSplit = defineWidget((ctx) => {
const [outerSizes, setOuterSizes] = useState([25, 75]);
const [innerSizes, setInnerSizes] = useState([60, 40]);
return ui.splitPane({
id: ctx.id("outer"),
direction: "horizontal",
sizes: outerSizes,
onResize: setOuterSizes,
}, [
// Left sidebar
ui.panel("Sidebar", [ui.text("Navigation")]),
// Right side: nested vertical split
ui.splitPane({
id: ctx.id("inner"),
direction: "vertical",
sizes: innerSizes,
onResize: setInnerSizes,
}, [
ui.panel("Main", [ui.text("Main content")]),
ui.panel("Console", [ui.text("Console output")]),
]),
]);
});
Three-Panel Layout
const ThreePanel = defineWidget((ctx) => {
const [sizes, setSizes] = useState([25, 50, 25]);
return ui.splitPane({
id: ctx.id("three-panel"),
direction: "horizontal",
sizes,
minSizes: [15, 30, 15],
onResize: setSizes,
}, [
ui.panel("Left", [ui.text("Left panel")]),
ui.panel("Center", [ui.text("Center panel")]),
ui.panel("Right", [ui.text("Right panel")]),
]);
});
Responsive Split
import { useTerminalSize } from "@rezi-ui/core";
const ResponsiveSplit = defineWidget((ctx) => {
const size = useTerminalSize();
const [sizes, setSizes] = useState([30, 70]);
// Switch to vertical layout on narrow terminals
const direction = size.cols < 80 ? "vertical" : "horizontal";
return ui.splitPane({
id: ctx.id("responsive"),
direction,
sizes,
onResize: setSizes,
}, [
ui.panel("Panel 1", [ui.text("Content 1")]),
ui.panel("Panel 2", [ui.text("Content 2")]),
]);
});
Divider Interaction
Mouse Drag
Click and drag divider to resize panels. The onResize callback receives updated sizes.
Size Clamping
Sizes are automatically clamped to respect:
- Minimum panel sizes
- Maximum panel sizes
- Available space
Deterministic Distribution
Rezi uses weighted integer distribution for stable, deterministic sizing:
// sizes: [33.33, 33.33, 33.34] percentages
// Available: 100 cells
// Result: [33, 33, 34] cells (no rounding errors)
Persistence
Save/Restore Sizes
import { defineWidget, useState, useEffect } from "@rezi-ui/core";
const PersistentSplit = defineWidget((ctx) => {
const [sizes, setSizes] = useState(() => {
const saved = localStorage.getItem("split-sizes");
return saved ? JSON.parse(saved) : [50, 50];
});
useEffect(() => {
localStorage.setItem("split-sizes", JSON.stringify(sizes));
}, [sizes]);
return ui.splitPane({
id: ctx.id("split"),
direction: "horizontal",
sizes,
onResize: setSizes,
}, panels);
});
- Resize events are throttled for smooth dragging
- Layout computation is O(n) where n = number of panels
- Divider hit testing uses expanded hit area for easy grabbing
See Also