Resizable
Panel layout where panels can be resized by dragging handles between them. Uses compound component API for flexible layouts.
Drag to ResizeCompound APIMin/Max BoundsHorizontal Layout
Install
$
npx react-principles add resizable01
Live Demo
Explore all variants and interactive states in Storybook.
Open Storybookopen_in_newBasic Horizontal Layout
Left Panel
Drag the handle to resize
Right Panel
Panels resize smoothly
Code Editor Layout
File Explorer
src/
components/
utils/
index.tsx
Code Editor
import React from 'react';
export default function App() {
return <div>Hello</div>;
}
02
Code Snippet
src/ui/Resizable.tsx
import { Resizable } from "@/ui/Resizable"; // Horizontal layout <Resizable direction="horizontal" className="h-96"> <Resizable.Panel defaultSize={50}> <div>Left panel</div> </Resizable.Panel> <Resizable.Handle /> <Resizable.Panel defaultSize={50}> <div>Right panel</div> </Resizable.Panel> </Resizable> // With visual grip <Resizable direction="horizontal" className="h-96"> <Resizable.Panel defaultSize={40}> <div>Sidebar</div> </Resizable.Panel> <Resizable.Handle withHandle /> <Resizable.Panel defaultSize={60}> <div>Main content</div> </Resizable.Panel> </Resizable> // Three panels <Resizable direction="horizontal" className="h-96"> <Resizable.Panel defaultSize={33.33}> <div>Panel 1</div> </Resizable.Panel> <Resizable.Handle /> <Resizable.Panel defaultSize={33.33}> <div>Panel 2</div> </Resizable.Panel> <Resizable.Handle /> <Resizable.Panel defaultSize={33.34}> <div>Panel 3</div> </Resizable.Panel> </Resizable>
03
Copy-Paste (Single File)
Resizable.tsx
import { cn } from "@/lib/utils"; import React, { useRef, useState, type ReactNode } from "react"; // ─── Types ──────────────────────────────────────────────────────────────────── export type ResizableDirection = "horizontal" | "vertical"; export interface ResizableProps { direction: ResizableDirection; children: ReactNode; className?: string; } export interface ResizablePanelProps { defaultSize?: number; minSize?: number; maxSize?: number; children: ReactNode; className?: string; } export interface ResizableHandleProps { withHandle?: boolean; disabled?: boolean; className?: string; } // ─── Main Component ─────────────────────────────────────────────────────────── export function Resizable({ direction, children, className }: ResizableProps) { const [sizes, setSizes] = useState<number[]>([]); const [activeHandle, setActiveHandle] = useState<number | null>(null); const containerRef = useRef<HTMLDivElement>(null); // Count panels and assign indices let panelCount = 0; const childrenWithProps = React.Children.map(children, (child) => { if (!React.isValidElement(child)) return child; if (child.type === Resizable.Panel) { const index = panelCount++; // Initialize size if (sizes[index] === undefined) { setSizes((prev) => { const next = [...prev]; next[index] = (child.props as ResizablePanelProps).defaultSize || 50; return next; }); } return React.cloneElement(child, { index, sizes, setSizes, containerRef, direction, activeHandle, setActiveHandle, } as any); } if (child.type === Resizable.Handle) { const handleIndex = panelCount - 1; return React.cloneElement(child, { index: handleIndex, sizes, setSizes, containerRef, direction, activeHandle, setActiveHandle, } as any); } return child; }); return ( <div ref={containerRef} className={cn("flex", direction === "horizontal" ? "flex-row" : "flex-col", className)} > {childrenWithProps} </div> ); } // ─── Panel Sub-Component ─────────────────────────────────────────────────────── Resizable.Panel = function ResizablePanel({ defaultSize = 50, children, className, index, sizes, }: ResizablePanelProps & { index?: number; sizes?: number[]; setSizes?: any; containerRef?: any; direction?: ResizableDirection; activeHandle?: number | null; setActiveHandle?: any; }) { const size = index !== undefined && sizes?.[index] !== undefined ? sizes[index] : defaultSize; return ( <div className={cn("flex-shrink-0", className)} style={{ flexBasis: `${size}%`, flexShrink: 0 }} > {children} </div> ); }; // ─── Handle Sub-Component ─────────────────────────────────────────────────────────── Resizable.Handle = function ResizableHandle({ withHandle = false, disabled = false, className, index, sizes, setSizes, containerRef, direction = "horizontal", activeHandle, }: ResizableHandleProps & { index?: number; sizes?: number[]; setSizes?: any; containerRef?: any; activeHandle?: number | null; }) { const isHorizontal = direction === "horizontal"; const handleMouseDown = (e: React.MouseEvent) => { if (disabled || index === undefined) return; e.preventDefault(); const container = containerRef.current; if (!container || !setSizes) return; const startX = e.clientX; const initialSizes = [...(sizes || [])]; setActiveHandle(index); const handleMouseMove = (moveEvent: MouseEvent) => { const containerWidth = container.offsetWidth; const deltaX = moveEvent.clientX - startX; const deltaPercent = (deltaX / containerWidth) * 100; if (index === undefined) return; const initialPanelSize = initialSizes[index] || 50; const newPanelSize = Math.max(10, Math.min(90, initialPanelSize + deltaPercent)); const newNextPanelSize = initialSizes[index + 1] - (newPanelSize - initialPanelSize); if (newNextPanelSize >= 10 && newNextPanelSize <= 90) { setSizes((prev: number[]) => { const next = [...prev]; next[index] = newPanelSize; next[index + 1] = newNextPanelSize; return next; }); } }; const handleMouseUp = () => { setActiveHandle(null); document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); }; return ( <div tabIndex={disabled ? -1 : 0} role="separator" aria-orientation={direction} className={cn( "flex-shrink-0 bg-slate-200 dark:bg-[#1f2937]", "hover:bg-primary/20", disabled && "opacity-50 cursor-not-allowed", !disabled && "select-none", !disabled && (isHorizontal ? "cursor-col-resize" : "cursor-row-resize"), isHorizontal ? "w-1" : "h-1", activeHandle === index && !disabled && "bg-primary", className )} onMouseDown={handleMouseDown} > {withHandle && ( <div className={cn("flex items-center justify-center", isHorizontal ? "h-full w-4" : "w-full h-4")}> <div className={cn("bg-slate-400 dark:bg-slate-600 rounded-full", isHorizontal ? "w-1 h-8" : "w-8 h-1")} /> </div> )} </div> ); };
04
Props
Resizable uses a compound component API with three components:
| Component | Prop | Type | Default | Description |
|---|---|---|---|---|
Resizable | direction | "horizontal" | "vertical" | — | Layout direction. Currently only horizontal is fully implemented. |
Resizable.Panel | defaultSize | number | 50 | Initial size of panel as percentage (0-100). |
Resizable.Panel | minSize | number | 10 | Minimum size as percentage. Panel won't shrink below this. |
Resizable.Panel | maxSize | number | 90 | Maximum size as percentage. Panel won't grow beyond this. |
Resizable.Handle | withHandle | boolean | false | Show visual grip/handle on the drag separator. |
Resizable.Handle | disabled | boolean | false | Disable the handle and prevent resizing. |
05
Usage Examples
Basic Two-Panel Layout
<Resizable direction="horizontal" className="h-96">
<Resizable.Panel defaultSize={50}>
<div>Left panel content</div>
</Resizable.Panel>
<Resizable.Handle withHandle />
<Resizable.Panel defaultSize={50}>
<div>Right panel content</div>
</Resizable.Panel>
</Resizable>Three-Panel Layout
<Resizable direction="horizontal" className="h-96">
<Resizable.Panel defaultSize={33.33}>
<div>Panel 1</div>
</Resizable.Panel>
<Resizable.Handle />
<Resizable.Panel defaultSize={33.33}>
<div>Panel 2</div>
</Resizable.Panel>
<Resizable.Handle />
<Resizable.Panel defaultSize={33.34}>
<div>Panel 3</div>
</Resizable.Panel>
</Resizable>