$ cat docs/resizable-panels.md
updated Recently · 28 min read · published
Resizable Panels ◆ HTML◆ CSS◆ Tailwind◆ Bootstrap◆ UI◆ Intermediate🎯 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 HTML CSS JS Tailwind Bootstrap Live
Copy 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>
Copy 1 .demo { 2 width:100%; 3 max-width:640px; 4 margin:0 auto; 5 background:#0A0A0A; 6 border:1px solid #222; 7 border-radius:8px; 8 overflow:hidden 9 } 10 .resizable { 11 display:flex; 12 height:220px; 13 width:100% 14 } 15 .panel.left { 16 width:240px; 17 flex-shrink:0; 18 padding:16px; 19 background:#111; 20 color:#A0A0A0; 21 font-family:system-ui, 22 sans-serif; 23 font-size:13px; 24 border-right:1px solid #222 25 } 26 .panel.right { 27 flex:1; 28 padding:16px; 29 background:#151515; 30 color:#A0A0A0; 31 font-family:system-ui, 32 sans-serif; 33 font-size:13px 34 } 35 .handle { 36 width:8px; 37 flex-shrink:0; 38 background:#222; 39 display:flex; 40 align-items:center; 41 justify-content:center; 42 cursor:col-resize; 43 transition:background .2s, 44 color .2s; 45 color:#555 46 } 47 .handle:hover, 48 .handle:focus { 49 background:#00FF41; 50 color:#0A0A0A; 51 outline:none 52 } 53 .grip { 54 width:12px; 55 height:12px; 56 pointer-events:none 57 }
untitled.js wrap JavaScript
Copy 1 (function(){const c=document.querySelector('.resizable');const l=c.querySelector('.panel.left');const h=c.querySelector('.handle');let d=false;const set=w=>{l.style.width=w+'px';h.setAttribute('aria-valuenow',w);};h.addEventListener('mousedown',e=>{d=true;e.preventDefault();});window.addEventListener('mousemove',e=>{if(!d)return;const r=c.getBoundingClientRect();const x=e.clientX-r.left;set(Math.max(120,Math.min(400,x)));});window.addEventListener('mouseup',()=>d=false);h.addEventListener('keydown',e=>{const s=e.shiftKey?20:10;let w=parseInt(getComputedStyle(l).width);if(e.key==='ArrowLeft')w=Math.max(120,w-s);if(e.key==='ArrowRight')w=Math.min(400,w+s);set(w);});})();
Copy 1 <div class="w-full max-w-[640px] mx-auto bg-[#0A0A0A] border border-[#222] rounded-lg overflow-hidden"> 2 <div class="resizable flex h-[220px] w-full"> 3 <div class="panel-left w-[240px] shrink-0 p-4 bg-[#111] text-[#A0A0A0] text-[13px] font-sans border-r border-[#222]"> 4 Sidebar 5 </div> 6 <div class="handle w-2 shrink-0 bg-[#222] flex items-center justify-center cursor-col-resize transition-colors text-[#555] hover:bg-[#00FF41] hover:text-[#0A0A0A] focus:bg-[#00FF41] focus:text-[#0A0A0A] focus:outline-none" role="separator" aria-valuenow="240" aria-valuemin="120" aria-valuemax="400" tabindex="0" aria-label="Resize sidebar"> 7 <svg class="w-3 h-3 pointer-events-none" 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 flex-1 p-4 bg-[#151515] text-[#A0A0A0] text-[13px] font-sans"> 17 Main content 18 </div> 19 </div> 20 </div>
Copy 1 <div class="container-fluid p-0" style="max-width:640px;"> 2 <div class="resizable d-flex" style="height:220px;background:#0A0A0A;border:1px solid #222;border-radius:8px;overflow:hidden;"> 3 <div class="panel-left" style="width:240px;flex-shrink:0;padding:16px;background:#111;color:#A0A0A0;font-size:13px;border-right:1px solid #222;"> 4 Sidebar 5 </div> 6 <div class="handle d-flex align-items-center justify-content-center" role="separator" aria-valuenow="240" aria-valuemin="120" aria-valuemax="400" tabindex="0" aria-label="Resize sidebar" style="width:8px;flex-shrink:0;background:#222;cursor:col-resize;color:#555;"> 7 <svg width="12" height="12" 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 flex-fill" style="padding:16px;background:#151515;color:#A0A0A0;font-size:13px;"> 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 HTML CSS JS Tailwind Bootstrap Live
Copy 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>
Copy 1 .demo { 2 width:100%; 3 max-width:640px; 4 margin:0 auto; 5 background:#0A0A0A; 6 border:1px solid #222; 7 border-radius:8px; 8 overflow:hidden 9 } 10 .resizable-v { 11 display:flex; 12 flex-direction:column; 13 height:280px; 14 width:100% 15 } 16 .panel.top { 17 height:120px; 18 flex-shrink:0; 19 padding:16px; 20 background:#111; 21 color:#A0A0A0; 22 font-family:system-ui, 23 sans-serif; 24 font-size:13px; 25 border-bottom:1px solid #222 26 } 27 .panel.bottom { 28 flex:1; 29 padding:16px; 30 background:#151515; 31 color:#A0A0A0; 32 font-family:system-ui, 33 sans-serif; 34 font-size:13px 35 } 36 .handle { 37 height:8px; 38 flex-shrink:0; 39 background:#222; 40 display:flex; 41 align-items:center; 42 justify-content:center; 43 cursor:row-resize; 44 transition:background .2s, 45 color .2s; 46 color:#555 47 } 48 .handle:hover, 49 .handle:focus { 50 background:#00FF41; 51 color:#0A0A0A; 52 outline:none 53 } 54 .grip { 55 width:12px; 56 height:12px; 57 pointer-events:none 58 }
untitled.js wrap JavaScript
Copy 1 (function(){const c=document.querySelector('.resizable-v');const t=c.querySelector('.panel.top');const h=c.querySelector('.handle');let d=false;const set=v=>{t.style.height=v+'px';h.setAttribute('aria-valuenow',v);};h.addEventListener('mousedown',e=>{d=true;e.preventDefault();});window.addEventListener('mousemove',e=>{if(!d)return;const r=c.getBoundingClientRect();const y=e.clientY-r.top;set(Math.max(60,Math.min(220,y)));});window.addEventListener('mouseup',()=>d=false);h.addEventListener('keydown',e=>{const s=e.shiftKey?20:10;let v=parseInt(getComputedStyle(t).height);if(e.key==='ArrowUp')v=Math.max(60,v-s);if(e.key==='ArrowDown')v=Math.min(220,v+s);set(v);});})();
Copy 1 <div class="w-full max-w-[640px] mx-auto bg-[#0A0A0A] border border-[#222] rounded-lg overflow-hidden"> 2 <div class="resizable-v flex flex-col h-[280px] w-full"> 3 <div class="panel-top h-[120px] shrink-0 p-4 bg-[#111] text-[#A0A0A0] text-[13px] font-sans border-b border-[#222]"> 4 Top panel 5 </div> 6 <div class="handle h-2 shrink-0 bg-[#222] flex items-center justify-center cursor-row-resize transition-colors text-[#555] hover:bg-[#00FF41] hover:text-[#0A0A0A] focus:bg-[#00FF41] focus:text-[#0A0A0A] focus:outline-none" role="separator" aria-valuenow="120" aria-valuemin="60" aria-valuemax="220" tabindex="0" aria-label="Resize top panel" aria-orientation="horizontal"> 7 <svg class="w-3 h-3 pointer-events-none" 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 flex-1 p-4 bg-[#151515] text-[#A0A0A0] text-[13px] font-sans"> 17 Bottom panel 18 </div> 19 </div> 20 </div>
Copy 1 <div class="container-fluid p-0" style="max-width:640px;"> 2 <div class="resizable-v d-flex flex-column" style="height:280px;background:#0A0A0A;border:1px solid #222;border-radius:8px;overflow:hidden;"> 3 <div class="panel-top" style="height:120px;flex-shrink:0;padding:16px;background:#111;color:#A0A0A0;font-size:13px;border-bottom:1px solid #222;"> 4 Top panel 5 </div> 6 <div class="handle d-flex align-items-center justify-content-center" role="separator" aria-valuenow="120" aria-valuemin="60" aria-valuemax="220" tabindex="0" aria-label="Resize top panel" aria-orientation="horizontal" style="height:8px;flex-shrink:0;background:#222;cursor:row-resize;color:#555;"> 7 <svg width="12" height="12" 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 flex-fill" style="padding:16px;background:#151515;color:#A0A0A0;font-size:13px;"> 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 HTML CSS JS Tailwind Bootstrap Live
Copy 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>
Copy 1 .demo { 2 width:100%; 3 max-width:640px; 4 margin:0 auto; 5 background:#0A0A0A; 6 border:1px solid #222; 7 border-radius:8px; 8 overflow:hidden 9 } 10 .resizable-c { 11 display:flex; 12 height:200px; 13 width:100% 14 } 15 .panel.left { 16 width:200px; 17 flex-shrink:0; 18 padding:16px; 19 background:#111; 20 color:#A0A0A0; 21 font-family:system-ui, 22 sans-serif; 23 font-size:13px; 24 border-right:1px solid #222 25 } 26 .panel.right { 27 flex:1; 28 padding:16px; 29 background:#151515; 30 color:#A0A0A0; 31 font-family:system-ui, 32 sans-serif; 33 font-size:13px 34 } 35 .handle { 36 width:8px; 37 flex-shrink:0; 38 background:#222; 39 display:flex; 40 align-items:center; 41 justify-content:center; 42 cursor:col-resize; 43 transition:background .2s, 44 color .2s; 45 color:#555 46 } 47 .handle:hover, 48 .handle:focus { 49 background:#00FF41; 50 color:#0A0A0A; 51 outline:none 52 } 53 .grip { 54 width:12px; 55 height:12px; 56 pointer-events:none 57 } 58 .hints { 59 display:flex; 60 justify-content:space-between; 61 padding:8px 12px; 62 background:#0A0A0A; 63 border-top:1px dashed #333 64 } 65 .hint { 66 font-size:10px; 67 font-family:system-ui, 68 sans-serif; 69 color:#808080 70 }
untitled.js wrap JavaScript
Copy 1 (function(){const MIN=120;const MAX=320;const c=document.querySelector('.resizable-c');const l=c.querySelector('.panel.left');const h=c.querySelector('.handle');let d=false;const set=w=>{l.style.width=w+'px';h.setAttribute('aria-valuenow',w);};h.addEventListener('mousedown',e=>{d=true;e.preventDefault();});window.addEventListener('mousemove',e=>{if(!d)return;const r=c.getBoundingClientRect();const x=e.clientX-r.left;set(Math.max(MIN,Math.min(MAX,x)));});window.addEventListener('mouseup',()=>d=false);h.addEventListener('keydown',e=>{const s=e.shiftKey?20:10;let w=parseInt(getComputedStyle(l).width);if(e.key==='ArrowLeft')w=Math.max(MIN,w-s);if(e.key==='ArrowRight')w=Math.min(MAX,w+s);set(w);});})();
Copy 1 <div class="w-full max-w-[640px] mx-auto bg-[#0A0A0A] border border-[#222] rounded-lg overflow-hidden"> 2 <div class="resizable-c flex h-[200px] w-full"> 3 <div class="panel-left w-[200px] shrink-0 p-4 bg-[#111] text-[#A0A0A0] text-[13px] font-sans border-r border-[#222]"> 4 Clamped sidebar 5 </div> 6 <div class="handle w-2 shrink-0 bg-[#222] flex items-center justify-center cursor-col-resize transition-colors text-[#555] hover:bg-[#00FF41] hover:text-[#0A0A0A] focus:bg-[#00FF41] focus:text-[#0A0A0A] focus:outline-none" role="separator" aria-valuenow="200" aria-valuemin="120" aria-valuemax="320" tabindex="0" aria-label="Resize clamped sidebar"> 7 <svg class="w-3 h-3 pointer-events-none" 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 flex-1 p-4 bg-[#151515] text-[#A0A0A0] text-[13px] font-sans"> 17 Drag me — I stop at the dashed lines 18 </div> 19 </div> 20 <div class="flex justify-between px-3 py-2 bg-[#0A0A0A] border-t border-dashed border-[#333]"> 21 <span class="text-[10px] font-sans text-[#808080]"> 22 min 120px 23 </span> 24 <span class="text-[10px] font-sans text-[#808080]"> 25 max 320px 26 </span> 27 </div> 28 </div>
Copy 1 <div class="container-fluid p-0" style="max-width:640px;"> 2 <div class="bg-[#0A0A0A]" style="background:#0A0A0A;border:1px solid #222;border-radius:8px;overflow:hidden;"> 3 <div class="resizable-c d-flex" style="height:200px;"> 4 <div class="panel-left" style="width:200px;flex-shrink:0;padding:16px;background:#111;color:#A0A0A0;font-size:13px;border-right:1px solid #222;"> 5 Clamped sidebar 6 </div> 7 <div class="handle d-flex align-items-center justify-content-center" role="separator" aria-valuenow="200" aria-valuemin="120" aria-valuemax="320" tabindex="0" aria-label="Resize clamped sidebar" style="width:8px;flex-shrink:0;background:#222;cursor:col-resize;color:#555;"> 8 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"> 9 <circle cx="9" cy="6" r="1.5"/> 10 <circle cx="9" cy="12" r="1.5"/> 11 <circle cx="9" cy="18" r="1.5"/> 12 <circle cx="15" cy="6" r="1.5"/> 13 <circle cx="15" cy="12" r="1.5"/> 14 <circle cx="15" cy="18" r="1.5"/> 15 </svg> 16 </div> 17 <div class="panel-right flex-fill" style="padding:16px;background:#151515;color:#A0A0A0;font-size:13px;"> 18 Drag me — I stop at the dashed lines 19 </div> 20 </div> 21 <div class="d-flex justify-content-between px-3 py-2" style="background:#0A0A0A;border-top:1px dashed #333;"> 22 <span class="text-secondary" style="font-size:10px;"> 23 min 120px 24 </span> 25 <span class="text-secondary" style="font-size:10px;"> 26 max 320px 27 </span> 28 </div> 29 </div> 30 </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 HTML CSS JS Tailwind Bootstrap Live
Copy 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>
Copy 1 .demo { 2 width:100%; 3 max-width:640px; 4 margin:0 auto; 5 background:#0A0A0A; 6 border:1px solid #222; 7 border-radius:8px; 8 overflow:hidden 9 } 10 .resizable-col { 11 display:flex; 12 height:220px; 13 width:100% 14 } 15 .panel.left { 16 width:200px; 17 flex-shrink:0; 18 padding:16px; 19 background:#111; 20 color:#A0A0A0; 21 font-family:system-ui, 22 sans-serif; 23 font-size:13px; 24 border-right:1px solid #222; 25 overflow:hidden; 26 transition:width .25s ease 27 } 28 .panel.right { 29 flex:1; 30 padding:16px; 31 background:#151515; 32 color:#A0A0A0; 33 font-family:system-ui, 34 sans-serif; 35 font-size:13px 36 } 37 .handle { 38 width:20px; 39 flex-shrink:0; 40 background:#222; 41 display:flex; 42 align-items:center; 43 justify-content:center; 44 cursor:col-resize; 45 position:relative; 46 transition:background .2s 47 } 48 .handle:hover, 49 .handle:focus { 50 background:#00FF41 51 } 52 .toggle { 53 position:absolute; 54 width:18px; 55 height:36px; 56 border:1px solid #333; 57 background:#151515; 58 color:#A0A0A0; 59 border-radius:6px; 60 display:flex; 61 align-items:center; 62 justify-content:center; 63 cursor:pointer; 64 padding:0; 65 transition:all .2s 66 } 67 .toggle:hover, 68 .toggle:focus { 69 color:#00FF41; 70 border-color:#00FF41; 71 outline:none 72 } 73 .chevron { 74 width:12px; 75 height:12px; 76 transition:transform .25s ease 77 } 78 .collapsed .chevron { 79 transform:rotate(180deg) 80 } 81 .collapsed .label { 82 opacity:0 83 } 84 .label { 85 transition:opacity .15s ease 86 }
untitled.js wrap JavaScript
Copy 1 (function(){const MIN=56;const MAX=320;const c=document.querySelector('.resizable-col');const l=c.querySelector('.panel.left');const h=c.querySelector('.handle');const t=h.querySelector('.toggle');let d=false;let expanded=true;let saved=200;const set=w=>{l.style.width=w+'px';h.setAttribute('aria-valuenow',w);};const toggle=()=>{expanded=!expanded;c.classList.toggle('collapsed',!expanded);t.setAttribute('aria-pressed',String(!expanded));t.setAttribute('aria-label',expanded?'Collapse sidebar':'Expand sidebar');set(expanded?saved:MIN);};h.addEventListener('mousedown',e=>{if(e.target.closest('.toggle'))return;d=true;e.preventDefault();});window.addEventListener('mousemove',e=>{if(!d)return;const r=c.getBoundingClientRect();const x=e.clientX-r.left;const w=Math.max(MIN,Math.min(MAX,x));saved=w;set(w);if(!expanded){expanded=true;c.classList.remove('collapsed');t.setAttribute('aria-pressed','false');t.setAttribute('aria-label','Collapse sidebar');}});window.addEventListener('mouseup',()=>d=false);h.addEventListener('keydown',e=>{if(e.target.closest('.toggle'))return;const s=e.shiftKey?20:10;let w=parseInt(getComputedStyle(l).width);if(e.key==='ArrowLeft')w=Math.max(MIN,w-s);if(e.key==='ArrowRight')w=Math.min(MAX,w+s);saved=w;set(w);if(!expanded){expanded=true;c.classList.remove('collapsed');t.setAttribute('aria-pressed','false');t.setAttribute('aria-label','Collapse sidebar');}});t.addEventListener('click',toggle);})();
Copy 1 <div class="w-full max-w-[640px] mx-auto bg-[#0A0A0A] border border-[#222] rounded-lg overflow-hidden"> 2 <div class="resizable-col flex h-[220px] w-full"> 3 <div class="panel-left w-[200px] shrink-0 p-4 bg-[#111] text-[#A0A0A0] text-[13px] font-sans border-r border-[#222] overflow-hidden transition-[width] duration-250 ease-out"> 4 <div class="label transition-opacity duration-150"> 5 Sidebar 6 </div> 7 </div> 8 <div class="handle w-5 shrink-0 bg-[#222] flex items-center justify-center cursor-col-resize relative transition-colors hover:bg-[#00FF41] focus:bg-[#00FF41] focus:outline-none" role="separator" aria-valuenow="200" aria-valuemin="56" aria-valuemax="320" tabindex="0" aria-label="Resize or collapse sidebar"> 9 <button class="toggle absolute w-[18px] h-9 border border-[#333] bg-[#151515] text-[#A0A0A0] rounded-md flex items-center justify-center cursor-pointer p-0 hover:text-[#00FF41] hover:border-[#00FF41] focus:outline-none" aria-label="Collapse sidebar" aria-pressed="false"> 10 <svg class="chevron w-3 h-3 transition-transform duration-250" 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 flex-1 p-4 bg-[#151515] text-[#A0A0A0] text-[13px] font-sans"> 16 Main content 17 </div> 18 </div> 19 </div>
Copy 1 <div class="container-fluid p-0" style="max-width:640px;"> 2 <div class="resizable-col d-flex" style="height:220px;background:#0A0A0A;border:1px solid #222;border-radius:8px;overflow:hidden;"> 3 <div class="panel-left" style="width:200px;flex-shrink:0;padding:16px;background:#111;color:#A0A0A0;font-size:13px;border-right:1px solid #222;overflow:hidden;transition:width .25s ease;"> 4 <div class="label" style="transition:opacity .15s ease;"> 5 Sidebar 6 </div> 7 </div> 8 <div class="handle d-flex align-items-center justify-content-center" role="separator" aria-valuenow="200" aria-valuemin="56" aria-valuemax="320" tabindex="0" aria-label="Resize or collapse sidebar" style="width:20px;flex-shrink:0;background:#222;cursor:col-resize;position:relative;transition:background .2s;"> 9 <button class="toggle" aria-label="Collapse sidebar" aria-pressed="false" style="position:absolute;width:18px;height:36px;border:1px solid #333;background:#151515;color:#A0A0A0;border-radius:6px;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:all .2s;"> 10 <svg class="chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="transition:transform .25s ease;"> 11 <polyline points="15 18 9 12 15 6"/> 12 </svg> 13 </button> 14 </div> 15 <div class="panel-right flex-fill" style="padding:16px;background:#151515;color:#A0A0A0;font-size:13px;"> 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 HTML CSS JS Tailwind Bootstrap Live
Copy 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>
Copy 1 .demo { 2 width:100%; 3 max-width:640px; 4 margin:0 auto; 5 background:#0A0A0A; 6 border:1px solid #222; 7 border-radius:8px; 8 overflow:hidden 9 } 10 .resizable-3 { 11 display:flex; 12 height:220px; 13 width:100% 14 } 15 .panel.left, 16 .panel.right { 17 width:160px; 18 flex-shrink:0; 19 padding:16px; 20 background:#111; 21 color:#A0A0A0; 22 font-family:system-ui, 23 sans-serif; 24 font-size:13px; 25 border-right:1px solid #222 26 } 27 .panel.right { 28 border-right:none; 29 border-left:1px solid #222 30 } 31 .panel.center { 32 flex:1; 33 padding:16px; 34 background:#151515; 35 color:#A0A0A0; 36 font-family:system-ui, 37 sans-serif; 38 font-size:13px 39 } 40 .handle { 41 width:8px; 42 flex-shrink:0; 43 background:#222; 44 display:flex; 45 align-items:center; 46 justify-content:center; 47 cursor:col-resize; 48 transition:background .2s, 49 color .2s; 50 color:#555 51 } 52 .handle:hover, 53 .handle:focus { 54 background:#00FF41; 55 color:#0A0A0A; 56 outline:none 57 } 58 .grip { 59 width:12px; 60 height:12px; 61 pointer-events:none 62 }
untitled.js wrap JavaScript
Copy 1 (function(){const MIN=100;const MAX=260;const c=document.querySelector('.resizable-3');const l=c.querySelector('.panel.left');const r=c.querySelector('.panel.right');const handles=Array.from(c.querySelectorAll('.handle'));let active=null;const set=(el,w)=>{el.style.width=w+'px';const h=handles.find(h=>h.dataset.side===(el===l?'left':'right'));h.setAttribute('aria-valuenow',w);};handles.forEach(h=>{h.addEventListener('mousedown',e=>{active=h;e.preventDefault();});h.addEventListener('keydown',e=>{const side=e.target.dataset.side;const el=side==='left'?l:r;const s=e.shiftKey?20:10;let w=parseInt(getComputedStyle(el).width);if(side==='left'){if(e.key==='ArrowLeft')w=Math.max(MIN,w-s);if(e.key==='ArrowRight')w=Math.min(MAX,w+s);}else{if(e.key==='ArrowLeft')w=Math.min(MAX,w+s);if(e.key==='ArrowRight')w=Math.max(MIN,w-s);}set(el,w);});});window.addEventListener('mousemove',e=>{if(!active)return;const rect=c.getBoundingClientRect();const center=rect.width-l.offsetWidth-r.offsetWidth;const side=active.dataset.side;if(side==='left'){const x=e.clientX-rect.left;set(l,Math.max(MIN,Math.min(Math.min(MAX,rect.width-r.offsetWidth-center-40),x)));}else{const x=rect.width-(e.clientX-rect.left);set(r,Math.max(MIN,Math.min(Math.min(MAX,rect.width-l.offsetWidth-center-40),x)));}});window.addEventListener('mouseup',()=>active=null);})();
Copy 1 <div class="w-full max-w-[640px] mx-auto bg-[#0A0A0A] border border-[#222] rounded-lg overflow-hidden"> 2 <div class="resizable-3 flex h-[220px] w-full"> 3 <div class="panel-left w-[160px] shrink-0 p-4 bg-[#111] text-[#A0A0A0] text-[13px] font-sans border-r border-[#222]"> 4 Explorer 5 </div> 6 <div class="handle w-2 shrink-0 bg-[#222] flex items-center justify-center cursor-col-resize transition-colors text-[#555] hover:bg-[#00FF41] hover:text-[#0A0A0A] focus:bg-[#00FF41] focus:text-[#0A0A0A] focus:outline-none" data-side="left" role="separator" aria-valuenow="160" aria-valuemin="100" aria-valuemax="260" tabindex="0" aria-label="Resize explorer"> 7 <svg class="w-3 h-3 pointer-events-none" 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 flex-1 p-4 bg-[#151515] text-[#A0A0A0] text-[13px] font-sans"> 17 Editor 18 </div> 19 <div class="handle w-2 shrink-0 bg-[#222] flex items-center justify-center cursor-col-resize transition-colors text-[#555] hover:bg-[#00FF41] hover:text-[#0A0A0A] focus:bg-[#00FF41] focus:text-[#0A0A0A] focus:outline-none" data-side="right" role="separator" aria-valuenow="160" aria-valuemin="100" aria-valuemax="260" tabindex="0" aria-label="Resize properties"> 20 <svg class="w-3 h-3 pointer-events-none" 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 w-[160px] shrink-0 p-4 bg-[#111] text-[#A0A0A0] text-[13px] font-sans border-l border-[#222]"> 30 Properties 31 </div> 32 </div> 33 </div>
Copy 1 <div class="container-fluid p-0" style="max-width:640px;"> 2 <div class="resizable-3 d-flex" style="height:220px;background:#0A0A0A;border:1px solid #222;border-radius:8px;overflow:hidden;"> 3 <div class="panel-left" style="width:160px;flex-shrink:0;padding:16px;background:#111;color:#A0A0A0;font-size:13px;border-right:1px solid #222;"> 4 Explorer 5 </div> 6 <div class="handle d-flex align-items-center justify-content-center" data-side="left" role="separator" aria-valuenow="160" aria-valuemin="100" aria-valuemax="260" tabindex="0" aria-label="Resize explorer" style="width:8px;flex-shrink:0;background:#222;cursor:col-resize;color:#555;"> 7 <svg width="12" height="12" 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 flex-fill" style="padding:16px;background:#151515;color:#A0A0A0;font-size:13px;"> 17 Editor 18 </div> 19 <div class="handle d-flex align-items-center justify-content-center" data-side="right" role="separator" aria-valuenow="160" aria-valuemin="100" aria-valuemax="260" tabindex="0" aria-label="Resize properties" style="width:8px;flex-shrink:0;background:#222;cursor:col-resize;color:#555;"> 20 <svg width="12" height="12" 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" style="width:160px;flex-shrink:0;padding:16px;background:#111;color:#A0A0A0;font-size:13px;border-left:1px solid #222;"> 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 wrap TypeScript
Copy 1 "use client"; 2 import { useState, useRef, useCallback, useEffect } from "react"; 3 4 interface ResizablePanelsProps { 5 left: React.ReactNode; 6 right: React.ReactNode; 7 defaultWidth?: number; 8 minWidth?: number; 9 maxWidth?: number; 10 } 11 12 function 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 wrap TypeScript
Copy 1 // Save panel sizes to localStorage 2 function 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 18 const [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