|$ curl https://forge-ai.dev/api/markdown?path=docs/components/resizable
$cat docs/resizable-panels.md
updated Recently·25 min read·published

Resizable Panels

UI ComponentsIntermediate🎯Free Tools
Introduction

Resizable panels let users adjust the width or height of content areas by dragging a handle. Common in IDEs, email clients, and dashboards. Key challenges: smooth dragging, min/max constraints, persistence, and keyboard accessibility.

Basic Resizable Panel
ResizablePanels.tsx
TypeScript
1"use client";
2import { useState, useRef, useCallback, useEffect } from "react";
3
4interface ResizableProps {
5 left: React.ReactNode;
6 right: React.ReactNode;
7 defaultWidth?: number;
8 minWidth?: number;
9 maxWidth?: number;
10}
11
12function ResizablePanels({ left, right, defaultWidth = 300, minWidth = 200, maxWidth = 600 }: ResizableProps) {
13 const [width, setWidth] = useState(defaultWidth);
14 const [isDragging, setIsDragging] = useState(false);
15 const containerRef = useRef<HTMLDivElement>(null);
16
17 const handleMouseDown = useCallback((e: React.MouseEvent) => {
18 e.preventDefault();
19 setIsDragging(true);
20 }, []);
21
22 useEffect(() => {
23 if (!isDragging) return;
24
25 const handleMouseMove = (e: MouseEvent) => {
26 if (!containerRef.current) return;
27 const rect = containerRef.current.getBoundingClientRect();
28 const newWidth = e.clientX - rect.left;
29 setWidth(Math.max(minWidth, Math.min(maxWidth, newWidth)));
30 };
31
32 const handleMouseUp = () => setIsDragging(false);
33
34 document.addEventListener("mousemove", handleMouseMove);
35 document.addEventListener("mouseup", handleMouseUp);
36 return () => {
37 document.removeEventListener("mousemove", handleMouseMove);
38 document.removeEventListener("mouseup", handleMouseUp);
39 };
40 }, [isDragging, minWidth, maxWidth]);
41
42 return (
43 <div ref={containerRef} className="flex h-full overflow-hidden">
44 <div style={{ width }} className="flex-shrink-0 overflow-auto border-r border-[#222]">
45 {left}
46 </div>
47 <div
48 role="separator"
49 aria-valuenow={width}
50 aria-valuemin={minWidth}
51 aria-valuemax={maxWidth}
52 tabIndex={0}
53 className="w-1 bg-[#333] hover:bg-[#00FF41] cursor-col-resize flex-shrink-0 focus:outline-none focus:bg-[#00FF41]"
54 onMouseDown={handleMouseDown}
55 onKeyDown={(e) => {
56 const step = e.shiftKey ? 50 : 10;
57 if (e.key === "ArrowLeft") setWidth((w) => Math.max(minWidth, w - step));
58 if (e.key === "ArrowRight") setWidth((w) => Math.min(maxWidth, w + step));
59 }}
60 />
61 <div className="flex-1 overflow-auto">{right}</div>
62 </div>
63 );
64}

info

Use role="separator" with aria-valuenow, aria-valuemin, and aria-valuemax for screen reader support. Arrow keys should resize the panel.
Persisting Panel Sizes
persistence.ts
TypeScript
1// Save panel sizes to localStorage
2function useResizableState(key: string, defaultWidth: number) {
3 const [width, setWidth] = useState(() => {
4 if (typeof window === "undefined") return defaultWidth;
5 const saved = localStorage.getItem(`panel-${key}`);
6 return saved ? parseInt(saved) : defaultWidth;
7 });
8
9 const handleWidthChange = useCallback((newWidth: number) => {
10 setWidth(newWidth);
11 localStorage.setItem(`panel-${key}`, String(newWidth));
12 }, [key]);
13
14 return [width, handleWidthChange] as const;
15}
16
17// Usage
18const [sidebarWidth, setSidebarWidth] = useResizableState("sidebar", 280);
$Blueprint — Engineering Documentation·Section ID: UI-RP-01·Revision: 1.0