SplitPane
MD · 1950B gzip budgetResizable two-panel layout with draggable divider.
- Category
- Media & layout
- Budget
- md, several states or a live subscription
- External runtime
- React / React DOM only
axe fixtureSSR rendersource scanbundle budget
Preview
Live fixtureTry the component here. Locale-aware formatting and direction follow the selected language; example content and labels are not automatically translated. Overlay demos use interactive launchers.
Install
npx shadcn@latest add https://gear5-ui.vercel.app/r/split-pane.jsonCopies the source into your project.
New here? Set up Tailwind and import aliases first →Source & props
Edit on GitHub ↗Prop types and inline documentation are included below. This is repository source; the installer rewrites shared imports for your project.
"use client";
import React, { useCallback, useRef, useState } from "react";
export interface SplitPaneProps {
left: React.ReactNode;
right: React.ReactNode;
defaultRatio?: number;
minSize?: string;
maxSize?: string;
direction?: "horizontal" | "vertical";
className?: string;
ariaLabel?: string;
}
export function SplitPane({
left,
right,
defaultRatio = 50,
minSize = "200px",
maxSize = "80%",
direction = "horizontal",
className = "",
ariaLabel,
}: SplitPaneProps) {
const [ratio, setRatio] = useState(defaultRatio);
const containerRef = useRef<HTMLDivElement>(null);
const isDragging = useRef(false);
const isHorizontal = direction === "horizontal";
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
isDragging.current = true;
const handleMouseMove = (moveEvent: MouseEvent) => {
if (!isDragging.current || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const clientPos = isHorizontal ? moveEvent.clientX : moveEvent.clientY;
const totalSize = isHorizontal ? rect.width : rect.height;
const offset = clientPos - (isHorizontal ? rect.left : rect.top);
const pct = Math.max(10, Math.min(90, (offset / totalSize) * 100));
setRatio(pct);
};
const handleMouseUp = () => {
isDragging.current = false;
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
document.body.style.cursor = isHorizontal ? "col-resize" : "row-resize";
document.body.style.userSelect = "none";
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
},
[isHorizontal]
);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
const step = 2;
if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
setRatio((r) => Math.max(10, r - step));
} else if (e.key === "ArrowRight" || e.key === "ArrowDown") {
setRatio((r) => Math.min(90, r + step));
}
},
[]
);
return (
<div
ref={containerRef}
role="group"
aria-label={ariaLabel}
className={className}
style={{
display: "flex",
flexDirection: isHorizontal ? "row" : "column",
height: "100%",
width: "100%",
overflow: "hidden",
}}
>
<div
style={{
flex: `0 0 ${ratio}%`,
overflow: "auto",
}}
>
{left}
</div>
<div
role="separator"
aria-orientation={isHorizontal ? "vertical" : "horizontal"}
aria-valuenow={Math.round(ratio)}
tabIndex={0}
onMouseDown={handleMouseDown}
onKeyDown={handleKeyDown}
style={{
flex: "0 0 6px",
cursor: isHorizontal ? "col-resize" : "row-resize",
background: "var(--color-border, #e5e7eb)",
touchAction: "none",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "background 0.15s",
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLDivElement).style.background = "var(--color-primary, #3b82f6)";
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLDivElement).style.background = "var(--color-border, #e5e7eb)";
}}
>
<div
style={{
width: isHorizontal ? 2 : 20,
height: isHorizontal ? 20 : 2,
borderRadius: 1,
background: "var(--color-text, #374151)",
}}
/>
</div>
<div
style={{
flex: `0 0 ${100 - ratio}%`,
overflow: "auto",
}}
>
{right}
</div>
</div>
);
}
What CI checks
Quality contract- Bundled, minified and gzipped against its tier budget
- Audited by axe in the state previewed above
- Rendered through react-dom/server with no browser globals
- Scanned for network calls, dangerous sinks, and unguarded animation