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

Resizable Panels

HTMLCSSTailwindBootstrapUIIntermediate🎯Free Tools
Introduction

Resizable panels let users adjust the width or height of content areas by dragging a handle. They are common in IDEs, email clients, dashboards, and file explorers. This guide covers production-ready patterns in plain HTML/CSS, Tailwind CSS, and Bootstrap — including horizontal, vertical, constrained, collapsible, and multi-panel layouts.

info

Always expose resize handles as role="separator" with aria-valuenow, aria-valuemin, and aria-valuemax. Arrow keys should resize the panel and focus styles must be visible.
Basic Horizontal Resizable

A classic two-column layout with a draggable vertical handle. Drag the handle or use the Left/Right arrow keys to resize.

basic-horizontal
Live
untitled.html
HTML
1<div class="demo">
2 <div class="resizable">
3 <div class="panel left">
4 Sidebar
5 </div>
6 <div class="handle" role="separator" aria-valuenow="240" aria-valuemin="120" aria-valuemax="400" tabindex="0" aria-label="Resize sidebar">
7 <svg class="grip" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
8 <circle cx="9" cy="6" r="1.5"/>
9 <circle cx="9" cy="12" r="1.5"/>
10 <circle cx="9" cy="18" r="1.5"/>
11 <circle cx="15" cy="6" r="1.5"/>
12 <circle cx="15" cy="12" r="1.5"/>
13 <circle cx="15" cy="18" r="1.5"/>
14 </svg>
15 </div>
16 <div class="panel right">
17 Main content
18 </div>
19 </div>
20</div>
preview
Vertical Resizable

Stack panels vertically and drag the horizontal separator to adjust height. Use Up/Down arrow keys for keyboard control.

vertical
Live
untitled.html
HTML
1<div class="demo">
2 <div class="resizable-v">
3 <div class="panel top">
4 Top panel
5 </div>
6 <div class="handle" role="separator" aria-valuenow="120" aria-valuemin="60" aria-valuemax="220" tabindex="0" aria-label="Resize top panel" aria-orientation="horizontal">
7 <svg class="grip" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
8 <circle cx="6" cy="9" r="1.5"/>
9 <circle cx="12" cy="9" r="1.5"/>
10 <circle cx="18" cy="9" r="1.5"/>
11 <circle cx="6" cy="15" r="1.5"/>
12 <circle cx="12" cy="15" r="1.5"/>
13 <circle cx="18" cy="15" r="1.5"/>
14 </svg>
15 </div>
16 <div class="panel bottom">
17 Bottom panel
18 </div>
19 </div>
20</div>
preview
Min / Max Constraints

Constrain the panel to a minimum and maximum size. The example below clamps the sidebar between 120px and 320px and shows a subtle boundary hint.

constraints
Live
untitled.html
HTML
1<div class="demo">
2 <div class="resizable-c">
3 <div class="panel left">
4 Clamped sidebar
5 </div>
6 <div class="handle" role="separator" aria-valuenow="200" aria-valuemin="120" aria-valuemax="320" tabindex="0" aria-label="Resize clamped sidebar">
7 <svg class="grip" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
8 <circle cx="9" cy="6" r="1.5"/>
9 <circle cx="9" cy="12" r="1.5"/>
10 <circle cx="9" cy="18" r="1.5"/>
11 <circle cx="15" cy="6" r="1.5"/>
12 <circle cx="15" cy="12" r="1.5"/>
13 <circle cx="15" cy="18" r="1.5"/>
14 </svg>
15 </div>
16 <div class="panel right">
17 Drag me — I stop at the dashed lines
18 </div>
19 </div>
20 <div class="hints">
21 <span class="hint">
22 min 120px
23 </span>
24 <span class="hint">
25 max 320px
26 </span>
27 </div>
28</div>
preview
Collapsible Sidebar

Add a toggle button to collapse the panel to an icon-only rail and restore it to its previous size. The toggle sits on the handle for discoverability.

collapsible
Live
untitled.html
HTML
1<div class="demo">
2 <div class="resizable-col">
3 <div class="panel left">
4 <div class="label">
5 Sidebar
6 </div>
7 </div>
8 <div class="handle" role="separator" aria-valuenow="200" aria-valuemin="56" aria-valuemax="320" tabindex="0" aria-label="Resize or collapse sidebar">
9 <button class="toggle" aria-label="Collapse sidebar" aria-pressed="false">
10 <svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
11 <polyline points="15 18 9 12 15 6"/>
12 </svg>
13 </button>
14 </div>
15 <div class="panel right">
16 Main content
17 </div>
18 </div>
19</div>
preview
Three-Panel Layout

A more realistic IDE-style layout with left and right resizable side panels and a flexible center area. Each handle has its own min/max bounds.

three-panel
Live
untitled.html
HTML
1<div class="demo">
2 <div class="resizable-3">
3 <div class="panel left">
4 Explorer
5 </div>
6 <div class="handle" data-side="left" role="separator" aria-valuenow="160" aria-valuemin="100" aria-valuemax="260" tabindex="0" aria-label="Resize explorer">
7 <svg class="grip" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
8 <circle cx="9" cy="6" r="1.5"/>
9 <circle cx="9" cy="12" r="1.5"/>
10 <circle cx="9" cy="18" r="1.5"/>
11 <circle cx="15" cy="6" r="1.5"/>
12 <circle cx="15" cy="12" r="1.5"/>
13 <circle cx="15" cy="18" r="1.5"/>
14 </svg>
15 </div>
16 <div class="panel center">
17 Editor
18 </div>
19 <div class="handle" data-side="right" role="separator" aria-valuenow="160" aria-valuemin="100" aria-valuemax="260" tabindex="0" aria-label="Resize properties">
20 <svg class="grip" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
21 <circle cx="9" cy="6" r="1.5"/>
22 <circle cx="9" cy="12" r="1.5"/>
23 <circle cx="9" cy="18" r="1.5"/>
24 <circle cx="15" cy="6" r="1.5"/>
25 <circle cx="15" cy="12" r="1.5"/>
26 <circle cx="15" cy="18" r="1.5"/>
27 </svg>
28 </div>
29 <div class="panel right">
30 Properties
31 </div>
32 </div>
33</div>
preview
React Implementation

A reusable React component that tracks mouse movement globally while dragging, supports min/max constraints, and handles keyboard resizing.

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

Save panel sizes to localStorage so the layout survives page reloads. Gate reads behind a typeof window check for SSR safety.

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);
Accessibility

Resizable interfaces are often mouse-centric, but keyboard and screen-reader users need equal control. Follow these practical checks.

  • Expose every drag handle as role="separator" with aria-valuenow, aria-valuemin, and aria-valuemax.
  • Make the handle focusable with tabIndex={0} and a visible focus state.
  • Support ArrowLeft / ArrowRight for horizontal panels and ArrowUp / ArrowDown for vertical panels.
  • Hold Shift to resize in larger steps (for example 20px instead of 10px).
  • Provide an aria-label describing what the handle resizes.
  • Ensure color is not the only indicator of the active handle; add a background change on hover and focus.
  • Respect reduced motion by disabling width/height transitions when prefers-reduced-motion is set.
  • Do not trap focus inside a resize handle; let Tab flow naturally through the page.
$Blueprint — Engineering Documentation·Section ID: UI-RP-01·Revision: 1.1

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.