Resizable
Accessible resizable panel groups and layouts with keyboard support.
Demo
Panel 1
Panel 2
Dependencies
clsxtailwind-merge
Props
This is a base primitive. It accepts standard HTML attributes plus className for styling via cn().
Source
"use client";
import { useRef, useState, useCallback, type ReactNode } from "react";
import { cn } from "@/lib/utils";
export interface ResizablePanelGroupProps {
direction?: "horizontal" | "vertical";
children: [ReactNode, ReactNode];
defaultSizes?: [number, number];
className?: string;
}
export function ResizablePanelGroup({
direction = "horizontal",
children,
defaultSizes = [50, 50],
className,
}: ResizablePanelGroupProps) {
const [sizes, setSizes] = useState(defaultSizes);
const dragging = useRef(false);
const containerRef = useRef<HTMLDivElement>(null);
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
dragging.current = true;
const startX = e.clientX;
const startY = e.clientY;
const startSizes: [number, number] = [sizes[0], sizes[1]];
const handleMouseMove = (ev: MouseEvent) => {
if (!dragging.current || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const delta =
direction === "horizontal"
? ev.clientX - startX
: ev.clientY - startY;
const total = direction === "horizontal" ? rect.width : rect.height;
const deltaPct = (delta / total) * 100;
const newSizes: [number, number] = [
Math.max(10, Math.min(90, startSizes[0]! + deltaPct)),
Math.max(10, Math.min(90, startSizes[1]! - deltaPct)),
];
setSizes(newSizes);
};
const handleMouseUp = () => {
dragging.current = false;
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
},
[sizes, direction],
);
return (
<div
ref={containerRef}
className={cn(
"flex",
direction === "horizontal" ? "flex-row" : "flex-col",
className,
)}
>
<div
style={{
[direction === "horizontal" ? "width" : "height"]: `${sizes[0]}%`,
}}
>
{children[0]}
</div>
<div
onMouseDown={handleMouseDown}
className={cn(
"flex shrink-0 cursor-col-resize items-center justify-center bg-gray-100 transition-colors hover:bg-gray-200",
direction === "horizontal"
? "w-2 cursor-col-resize"
: "h-2 cursor-row-resize",
)}
/>
<div
style={{
[direction === "horizontal" ? "width" : "height"]: `${sizes[1]}%`,
}}
>
{children[1]}
</div>
</div>
);
}
CLI
npx genui add resizable