Shadcn.io is not affiliated with official shadcn/ui
Shadcn Resizable React resizable for adjustable panel layouts and splitview interfaces with drag handles. Built with TypeScript and Tailwind CSS for Next.js.
Ever built an admin dashboard where half the screen was wasted space? Or used an app where the sidebar covered everything important and you couldn't resize it? Yeah, that's exactly why fixed layouts frustrate users. This shadcn/ui resizable component brings VS Code-style adjustable panels to your React applications.
Drag the handles to create your perfect layout:
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "~/components/ui/resizable" export default function ResizableDemo() { return ( <div className="flex justify-center self-start pt-6 w-full" style={{ all: "revert", display: "flex", justifyContent: "center", alignSelf: "flex-start", paddingTop: "1.5rem", width: "100%", fontSize: "14px", lineHeight: "1.5", letterSpacing: "normal", }} > <ResizablePanelGroup className="max-w-md rounded-lg border md:min-w-[450px]" direction="horizontal" > <ResizablePanel defaultSize={50}> <div className="flex h-[200px] items-center justify-center p-6"> <span className="font-semibold">One</span> </div> </ResizablePanel> <ResizableHandle /> <ResizablePanel defaultSize={50}> <ResizablePanelGroup direction="vertical"> <ResizablePanel defaultSize={25}> <div className="flex h-full items-center justify-center p-6"> <span className="font-semibold">Two</span> </div> </ResizablePanel> <ResizableHandle /> <ResizablePanel defaultSize={75}> <div className="flex h-full items-center justify-center p-6"> <span className="font-semibold">Three</span> </div> </ResizablePanel> </ResizablePanelGroup> </ResizablePanel> </ResizablePanelGroup> </div> ) }
Built on react-resizable-panels with keyboard navigation, smooth animations, and all the constraint handling you need for professional interfaces. Styled with Tailwind CSS to match your design system instead of looking like a generic system splitter from Windows 95.
npx shadcn@latest add resizable
Here's the thing—everyone works differently, and fixed layouts never fit anyone perfectly. Think about how VS Code lets you resize the file explorer, or how Figma's panels adapt to your workflow. Users want control over their workspace, not layouts designed for some mythical "average" person.
Resizable panels eliminate the one-size-fits-none problem by letting users create layouts that actually work for their tasks and screen sizes. No more squinting at cramped content or scrolling endlessly because someone decided 300px was the perfect sidebar width. Plus they're accessible with keyboard navigation and remember user preferences across sessions.
This free shadcn resizable component handles the complex parts—drag mechanics, collision detection, persistence, keyboard control—while you focus on creating interfaces that adapt to users. Whether you're building admin dashboards, code editors, or data visualization tools in your Next.js applications, panels that resize intuitively keep users productive in your JavaScript projects.
Classic IDE-style with file tree and main content:
"use client" import { ChevronDown, ChevronRight, File, Folder } from "lucide-react" import * as React from "react" import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "~/components/ui/resizable" import { ScrollArea } from "~/components/ui/scroll-area" export default function ResizableEditor() { const [openFolders, setOpenFolders] = React.useState<Set<string>>(new Set(["src"])) const toggleFolder = (folder: string) => { const newOpen = new Set(openFolders) if (newOpen.has(folder)) { newOpen.delete(folder) } else { newOpen.add(folder) } setOpenFolders(newOpen) } return ( <div className="flex justify-center self-start pt-6 w-full" style={{ all: "revert", display: "flex", justifyContent: "center", alignSelf: "flex-start", paddingTop: "1.5rem", width: "100%", fontSize: "14px", lineHeight: "1.5", letterSpacing: "normal", }} > <ResizablePanelGroup className="h-[400px] max-w-5xl rounded-lg border" direction="horizontal"> <ResizablePanel defaultSize={20} maxSize={30} minSize={15}> <div className="h-full bg-muted/30"> <div className="border-b px-3 py-2"> <h3 className="text-sm font-semibold">Explorer</h3> </div> <ScrollArea className="h-[calc(100%-32px)]"> <div className="p-2 text-sm"> <div> <button type="button" className="flex items-center gap-1 w-full hover:bg-accent/50 rounded px-1 py-0.5" onClick={() => toggleFolder("src")} > {openFolders.has("src") ? ( <ChevronDown className="h-3 w-3" /> ) : ( <ChevronRight className="h-3 w-3" /> )} <Folder className="h-3.5 w-3.5" /> <span>src</span> </button> {openFolders.has("src") && ( <div className="ml-3 mt-1 space-y-1"> <div className="flex items-center gap-1 hover:bg-accent/50 rounded px-1 py-0.5"> <File className="h-3.5 w-3.5" /> <span>index.tsx</span> </div> <div className="flex items-center gap-1 hover:bg-accent/50 rounded px-1 py-0.5"> <File className="h-3.5 w-3.5" /> <span>app.tsx</span> </div> <div className="flex items-center gap-1 hover:bg-accent/50 rounded px-1 py-0.5"> <File className="h-3.5 w-3.5" /> <span>globals.css</span> </div> </div> )} </div> <div className="mt-2"> <div className="flex items-center gap-1 hover:bg-accent/50 rounded px-1 py-0.5"> <File className="h-3.5 w-3.5" /> <span>package.json</span> </div> <div className="flex items-center gap-1 hover:bg-accent/50 rounded px-1 py-0.5"> <File className="h-3.5 w-3.5" /> <span>README.md</span> </div> </div> </div> </ScrollArea> </div> </ResizablePanel> <ResizableHandle withHandle /> <ResizablePanel defaultSize={80}> <ResizablePanelGroup direction="vertical"> <ResizablePanel defaultSize={70}> <div className="h-full p-4 font-mono text-sm"> <div className="text-muted-foreground mb-2">{"// index.tsx"}</div> <div> <span className="text-blue-500">import</span> React{" "} <span className="text-blue-500">from</span>{" "} <span className="text-green-500">'react'</span> </div> <div className="mt-2"> <span className="text-blue-500">export default function</span>{" "} <span className="text-yellow-500">App</span>() {"{"} </div> <div className="ml-4"> <span className="text-blue-500">return</span> ( </div> <div className="ml-8"> <<span className="text-red-500">div</span>> </div> <div className="ml-12">Hello World</div> <div className="ml-8"> </<span className="text-red-500">div</span>> </div> <div className="ml-4">)</div> <div>{"}"}</div> </div> </ResizablePanel> <ResizableHandle /> <ResizablePanel defaultSize={30}> <div className="h-full bg-muted/30 p-3"> <div className="text-sm font-semibold mb-2">Terminal</div> <div className="font-mono text-xs space-y-1"> <div className="text-muted-foreground">$ npm run dev</div> <div className="text-green-500">✓ Ready in 2.1s</div> <div className="text-blue-500">→ Local: http://localhost:3000</div> </div> </div> </ResizablePanel> </ResizablePanelGroup> </ResizablePanel> </ResizablePanelGroup> </div> ) }
Admin panels that adapt to workflow needs:
"use client" import { BarChart3, Calendar, Home, Mail, PanelLeft, PanelLeftClose, Settings, Users, } from "lucide-react" import * as React from "react" import { Button } from "~/components/ui/button" import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "~/components/ui/resizable" import { cn } from "~/lib/utils" export default function ResizableSidebar() { const [isCollapsed, setIsCollapsed] = React.useState(false) const menuItems = [ { icon: Home, label: "Dashboard", active: true }, { icon: Users, label: "Users" }, { icon: Mail, label: "Messages", badge: "3" }, { icon: BarChart3, label: "Analytics" }, { icon: Calendar, label: "Calendar" }, { icon: Settings, label: "Settings" }, ] return ( <div className="flex justify-center self-start pt-6 w-full" style={{ all: "revert", display: "flex", justifyContent: "center", alignSelf: "flex-start", paddingTop: "1.5rem", width: "100%", fontSize: "14px", lineHeight: "1.5", letterSpacing: "normal", }} > <ResizablePanelGroup className="h-[400px] max-w-5xl rounded-lg border" direction="horizontal"> <ResizablePanel collapsedSize={4} collapsible={true} defaultSize={20} maxSize={30} minSize={15} onCollapse={() => setIsCollapsed(true)} onExpand={() => setIsCollapsed(false)} > <div className="h-full bg-muted/30"> <div className="flex items-center justify-between border-b px-3 py-2"> {!isCollapsed && <h3 className="text-sm font-semibold">Navigation</h3>} <Button className="h-6 w-6" onClick={() => setIsCollapsed(!isCollapsed)} size="icon" variant="ghost" > {isCollapsed ? ( <PanelLeft className="h-4 w-4" /> ) : ( <PanelLeftClose className="h-4 w-4" /> )} </Button> </div> <nav className="p-2"> {menuItems.map((item, i) => { const Icon = item.icon return ( <button type="button" className={cn( "w-full flex items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent/50 transition-colors", item.active && "bg-accent", isCollapsed && "justify-center", )} key={i} > <Icon className="h-4 w-4 flex-shrink-0" /> {!isCollapsed && ( <> <span className="flex-1 text-left">{item.label}</span> {item.badge && ( <span className="bg-primary text-primary-foreground text-xs px-1.5 py-0.5 rounded-full"> {item.badge} </span> )} </> )} </button> ) })} </nav> </div> </ResizablePanel> <ResizableHandle withHandle /> <ResizablePanel defaultSize={80}> <div className="h-full p-6"> <h1 className="text-2xl font-bold mb-4">Dashboard</h1> <div className="grid gap-4 md:grid-cols-2"> <div className="rounded-lg border p-4"> <h3 className="font-semibold mb-2">Total Users</h3> <p className="text-3xl font-bold">1,234</p> <p className="text-sm text-muted-foreground">+12% from last month</p> </div> <div className="rounded-lg border p-4"> <h3 className="font-semibold mb-2">Revenue</h3> <p className="text-3xl font-bold">$45,678</p> <p className="text-sm text-muted-foreground">+8% from last month</p> </div> <div className="rounded-lg border p-4"> <h3 className="font-semibold mb-2">Active Sessions</h3> <p className="text-3xl font-bold">573</p> <p className="text-sm text-muted-foreground">Currently online</p> </div> <div className="rounded-lg border p-4"> <h3 className="font-semibold mb-2">Conversion Rate</h3> <p className="text-3xl font-bold">3.4%</p> <p className="text-sm text-muted-foreground">+0.5% from last week</p> </div> </div> </div> </ResizablePanel> </ResizablePanelGroup> </div> ) }
Email client style with navigation, list, and detail:
"use client" import { Archive, Trash2 } from "lucide-react" import * as React from "react" import { Avatar, AvatarFallback } from "~/components/ui/avatar" import { Badge } from "~/components/ui/badge" import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "~/components/ui/resizable" import { ScrollArea } from "~/components/ui/scroll-area" export default function ResizableThreeColumn() { const [selectedEmail, setSelectedEmail] = React.useState(0) const folders = [ { name: "Inbox", count: 12, icon: "📥" }, { name: "Sent", count: 0, icon: "📤" }, { name: "Drafts", count: 2, icon: "📝" }, { name: "Starred", count: 5, icon: "⭐" }, { name: "Archive", count: 0, icon: "📁" }, { name: "Trash", count: 0, icon: "🗑️" }, ] const emails = [ { id: 0, from: "GitHub", subject: "Security alert: new sign-in", preview: "We noticed a new sign-in to your account from a device...", time: "2 hours ago", unread: true, avatar: "GH", }, { id: 1, from: "Vercel", subject: "Your deployment is ready", preview: "Your project has been successfully deployed to production...", time: "5 hours ago", unread: true, avatar: "V", }, { id: 2, from: "Linear", subject: "Sprint planning reminder", preview: "Don't forget about tomorrow's sprint planning session...", time: "Yesterday", unread: false, avatar: "L", }, { id: 3, from: "Stripe", subject: "Monthly invoice", preview: "Your monthly invoice for December is now available...", time: "2 days ago", unread: false, avatar: "S", }, ] return ( <div className="flex justify-center self-start pt-6 w-full" style={{ all: "revert", display: "flex", justifyContent: "center", alignSelf: "flex-start", paddingTop: "1.5rem", width: "100%", fontSize: "14px", lineHeight: "1.5", letterSpacing: "normal", }} > <ResizablePanelGroup className="h-[400px] max-w-6xl rounded-lg border" direction="horizontal"> <ResizablePanel defaultSize={15} maxSize={20} minSize={10}> <div className="h-full bg-muted/30"> <div className="border-b px-3 py-2"> <h3 className="text-sm font-semibold">Folders</h3> </div> <div className="p-2 space-y-1"> {folders.map((folder, i) => ( <button type="button" className={`w-full flex items-center justify-between rounded-md px-2 py-1.5 text-sm hover:bg-accent/50 transition-colors ${ i === 0 ? "bg-accent" : "" }`} key={i} > <div className="flex items-center gap-2"> <span>{folder.icon}</span> <span>{folder.name}</span> </div> {folder.count > 0 && ( <Badge className="h-5 px-1.5" variant="secondary"> {folder.count} </Badge> )} </button> ))} </div> </div> </ResizablePanel> <ResizableHandle /> <ResizablePanel defaultSize={35} maxSize={45} minSize={25}> <div className="h-full"> <div className="border-b px-3 py-2 flex items-center justify-between"> <h3 className="text-sm font-semibold">Inbox</h3> <div className="flex gap-1"> <Archive className="h-4 w-4 text-muted-foreground" /> <Trash2 className="h-4 w-4 text-muted-foreground" /> </div> </div> <ScrollArea className="h-[calc(100%-40px)]"> <div className="divide-y"> {emails.map(email => ( <button type="button" className={`w-full px-3 py-3 text-left hover:bg-accent/50 transition-colors ${ selectedEmail === email.id ? "bg-accent/50" : "" }`} key={email.id} onClick={() => setSelectedEmail(email.id)} > <div className="flex items-start gap-3"> <Avatar className="h-8 w-8"> <AvatarFallback className="text-xs">{email.avatar}</AvatarFallback> </Avatar> <div className="flex-1 min-w-0"> <div className="flex items-center justify-between mb-1"> <span className={`text-sm ${email.unread ? "font-semibold" : ""}`}> {email.from} </span> <span className="text-xs text-muted-foreground">{email.time}</span> </div> <div className={`text-sm ${email.unread ? "font-medium" : ""} truncate`}> {email.subject} </div> <div className="text-xs text-muted-foreground truncate"> {email.preview} </div> </div> </div> </button> ))} </div> </ScrollArea> </div> </ResizablePanel> <ResizableHandle /> <ResizablePanel defaultSize={50}> <div className="h-full p-6"> <div className="mb-4"> <h2 className="text-xl font-semibold mb-2">{emails[selectedEmail].subject}</h2> <div className="flex items-center gap-3 text-sm text-muted-foreground"> <div className="flex items-center gap-2"> <Avatar className="h-6 w-6"> <AvatarFallback className="text-xs"> {emails[selectedEmail].avatar} </AvatarFallback> </Avatar> <span>{emails[selectedEmail].from}</span> </div> <span>•</span> <span>{emails[selectedEmail].time}</span> </div> </div> <div className="prose prose-sm max-w-none"> <p>Hi there,</p> <p>{emails[selectedEmail].preview}</p> <p> This is the full email content that would normally be much longer and contain all the details of the message. In a real application, this would be the complete email body with formatting, links, and possibly attachments. </p> <p> Best regards, <br /> The {emails[selectedEmail].from} Team </p> </div> <div className="mt-6 flex gap-2"> <button type="button" className="px-3 py-1.5 text-sm border rounded-md hover:bg-accent" > Reply </button> <button type="button" className="px-3 py-1.5 text-sm border rounded-md hover:bg-accent" > Forward </button> <button type="button" className="px-3 py-1.5 text-sm border rounded-md hover:bg-accent" > Archive </button> </div> </div> </ResizablePanel> </ResizablePanelGroup> </div> ) }
Charts and controls that users can arrange:
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "~/components/ui/resizable" export default function ResizableDemo() { return ( <div className="flex justify-center self-start pt-6 w-full" style={{ all: "revert", display: "flex", justifyContent: "center", alignSelf: "flex-start", paddingTop: "1.5rem", width: "100%", fontSize: "14px", lineHeight: "1.5", letterSpacing: "normal", }} > <ResizablePanelGroup className="max-w-md rounded-lg border md:min-w-[450px]" direction="horizontal" > <ResizablePanel defaultSize={50}> <div className="flex h-[200px] items-center justify-center p-6"> <span className="font-semibold">One</span> </div> </ResizablePanel> <ResizableHandle /> <ResizablePanel defaultSize={50}> <ResizablePanelGroup direction="vertical"> <ResizablePanel defaultSize={25}> <div className="flex h-full items-center justify-center p-6"> <span className="font-semibold">Two</span> </div> </ResizablePanel> <ResizableHandle /> <ResizablePanel defaultSize={75}> <div className="flex h-full items-center justify-center p-6"> <span className="font-semibold">Three</span> </div> </ResizablePanel> </ResizablePanelGroup> </ResizablePanel> </ResizablePanelGroup> </div> ) }
Side-by-side content comparison and editing:
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "~/components/ui/resizable" export default function ResizableDemo() { return ( <div className="flex justify-center self-start pt-6 w-full" style={{ all: "revert", display: "flex", justifyContent: "center", alignSelf: "flex-start", paddingTop: "1.5rem", width: "100%", fontSize: "14px", lineHeight: "1.5", letterSpacing: "normal", }} > <ResizablePanelGroup className="max-w-md rounded-lg border md:min-w-[450px]" direction="horizontal" > <ResizablePanel defaultSize={50}> <div className="flex h-[200px] items-center justify-center p-6"> <span className="font-semibold">One</span> </div> </ResizablePanel> <ResizableHandle /> <ResizablePanel defaultSize={50}> <ResizablePanelGroup direction="vertical"> <ResizablePanel defaultSize={25}> <div className="flex h-full items-center justify-center p-6"> <span className="font-semibold">Two</span> </div> </ResizablePanel> <ResizableHandle /> <ResizablePanel defaultSize={75}> <div className="flex h-full items-center justify-center p-6"> <span className="font-semibold">Three</span> </div> </ResizablePanel> </ResizablePanelGroup> </ResizablePanel> </ResizablePanelGroup> </div> ) }
This free open source resizable component includes everything you need:
TypeScript-first - Full type safety with panel events and constraint management
React-resizable-panels powered - Battle-tested drag mechanics and performance
Keyboard accessible - Tab navigation and arrow key resizing with fine control
Tailwind CSS styled - Customize with utilities, not fighting component CSS
Smooth animations - No janky movements or flickering during resize
Constraint system - Min/max sizes that respect each other and content
Persistence support - Save and restore user layouts across sessions
Touch optimized - Works naturally on tablets and touch devices
Component Purpose Key Props ResizablePanelGroupContainer for panels direction, autoSaveId for persistenceResizablePanelIndividual panel defaultSize, minSize, maxSize, collapsibleResizableHandleDrag divider withHandle for visible grip, disabled
Setting Type Purpose defaultSizenumber Initial panel size as percentage minSize / maxSizenumber Size constraints to prevent unusable layouts collapsibleboolean Allow panel to collapse completely autoSaveIdstring Persist layout to localStorage with this key
Configuration Use Case Implementation 50/50 split Equal content areas defaultSize={50} on both panelsSidebar layout Navigation + content defaultSize={20} and 80Collapsible panel Hide when not needed collapsible={true} with collapse controls
Start with layouts that work for most users. This free shadcn/ui resizable component lets users customize, but provide sensible defaults that don't require immediate adjustment. This TypeScript component handles the mechanics—you provide starting layouts that make sense for common workflows in your Next.js applications.
Set meaningful constraints on panel sizes. Don't let panels collapse to unusable dimensions or expand beyond their content needs. This open source shadcn component respects your min/max settings—use them to prevent layouts that break your interface or hide important content.
Make resize handles discoverable and accessible. Users need visual cues that panels can be resized, especially on first use. This JavaScript component supports visible handles and hover states—combine with good visual design to guide user discovery of resizable functionality.
Consider mobile and touch interactions carefully. Resizable panels work on touch devices, but small screens often need different layout strategies. The Tailwind CSS styled component handles touch events—consider disabling resize on mobile or providing layout presets instead.
Persist user layouts when it matters. For productivity apps where users spend significant time, save their preferred layouts. This component includes localStorage persistence—use it for applications where users develop workflow preferences and layout habits.
Resizable panels naturally work with Sheet components for mobile-friendly alternatives and Tabs components for switching between panel content in your React applications. Use Button components for layout presets and collapse controls.
For complex interfaces, combine resizable panels with Card components to organize panel content or ScrollArea components for long content lists. This open source pattern creates professional dashboard and editor experiences.
When building admin tools, pair resizable panels with DataTable components for data-heavy interfaces or Dialog components for configuration without losing layout context. Separator components help organize complex panel content visually.
For development tools, use resizable panels with Command components for searchable file trees or Breadcrumb components for navigation context. Your JavaScript application can compose these shadcn components while maintaining intuitive layout control.