Resizable
Draggable split panels built on react-resizable-panels - horizontal or vertical groups, an optional grip, and free nesting for editor-style layouts.
"use client"
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"
export default function ResizableDemo() {
return (
<ResizablePanelGroup
direction="horizontal"
className="h-48 w-full max-w-md rounded-xl border border-border"
>
<ResizablePanel defaultSize={35}>
<div className="flex h-full items-center justify-center p-4 text-sm font-medium">
Sidebar
</div>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={65}>
<div className="flex h-full items-center justify-center p-4 text-sm text-muted-foreground">
Drag the handle to resize
</div>
</ResizablePanel>
</ResizablePanelGroup>
)
}Installation
Run the following command
npx shadcn@latest add @ai2/resizableDependencies including react-resizable-panels, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install react-resizable-panels@^2.1.9 lucide-react@^1.23.0Add the cn util
lib/utils.tsimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/* Adds a source-attribution ref param to a URL (the inspiration exports mark their
outbound links with an ai2.design attribution). An invalid URL is returned as is.
This file is SHOWN TO THE CONSUMER: the docs component pages render the source of
`cn` in a code block, so a Turkish comment here would reach every one of those
pages. Keep it English. */
export function withRef(url: string, ref = "ai2.design"): string {
try {
const u = new URL(url)
u.searchParams.set("ref", ref)
return u.toString()
} catch {
return url
}
}Copy the source code
components/ui/resizable.tsx"use client"
import type * as React from "react"
import { GripVertical } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
return (
<ResizablePrimitive.PanelGroup
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
}
function ResizablePanel(
props: React.ComponentProps<typeof ResizablePrimitive.Panel>
) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
}
function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) {
return (
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
/* The separator stays 1px VISUALLY while its DRAG AREA becomes
24px: after:w-1 (4px) was too thin to grab with a finger
(measured). Without touch-action-none the browser reads the drag
gesture as page scrolling and leaves the component completely
unusable on mobile. */
"relative flex w-px touch-none items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-6 after:-translate-x-1/2 focus-visible:outline-hidden focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-6 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border border-border bg-border">
<GripVertical className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
}
export { ResizableHandle, ResizablePanel, ResizablePanelGroup }This component wraps react-resizable-panels, so install that package first. Manual installs skip the @ai2/tokens theme - add the token CSS from the theming guide or the tone colors will be missing.
Usage
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"
<ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={35}>Sidebar</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={65}>Content</ResizablePanel>
</ResizablePanelGroup>Set direction on the group, give each ResizablePanel a defaultSize in percent, and place a ResizableHandle between them. Give the group a height or the panels collapse to zero.
Examples
Horizontal
Vertical
With and without a handle
The seam is draggable either way. Pass withHandle to show a grip; omit it for a bare rule.
Nested groups
Props
Every part also forwards its underlying react-resizable-panels props - for example onLayout on the group and onResize on a panel.
ResizablePanelGroup
| Prop | Type | Default | Description |
|---|---|---|---|
direction | "horizontal" | "vertical" | - | Required. Lays the panels out in a row (horizontal) or a column (vertical). Drives the data-panel-group-direction attribute the handle reads. |
autoSaveId | string | undefined | Persists panel sizes under this key so the layout is restored on reload (forwarded to react-resizable-panels). |
ResizablePanel
| Prop | Type | Default | Description |
|---|---|---|---|
defaultSize | number | undefined | Starting size of the panel as a percentage of the group (0 to 100). Sizes across a group should add up to 100. |
minSize | number | undefined | Lower bound in percent the panel cannot be dragged below. |
maxSize | number | undefined | Upper bound in percent the panel cannot be dragged above. |
collapsible | boolean | false | Allows the panel to collapse to collapsedSize when dragged past its minSize (forwarded to react-resizable-panels). |
ResizableHandle
| Prop | Type | Default | Description |
|---|---|---|---|
withHandle | boolean | false | Renders a visible grip in the center of the divider. Without it the whole seam is still draggable, just unmarked. |
disabled | boolean | false | Locks the handle so the adjacent panels cannot be resized. |
ai2 Resizable: draggable split panels for React layouts
The ai2 Resizable is a shadcn-compatible set of split-panel primitives for React, built on react-resizable-panels and styled with Tailwind CSS v4. It lets users drag a divider to resize adjacent regions, the way a code editor splits a file tree from the editor from a terminal. Groups run horizontally or vertically and nest freely.
Because it ships through the shadcn registry format, you install it with one CLI command, an MCP agent, or a copy-paste, and the source lands in your own project. You own the file; there is no runtime dependency on ai2 itself. The live example above is the exact component you get.
What is the ai2 Resizable?
It is a three-part composition: ResizablePanelGroup, ResizablePanel and ResizableHandle. The group sets the direction, each panel takes a defaultSize in percent, and a handle sits between panels as the draggable seam. The anatomy matches shadcn/ui exactly, so existing snippets and AI agents keep working without changes.
The handle is a thin border by default; pass withHandle to show a small grip so users can find it. Sizes are percentages that add up to 100 within a group, and panels accept minSize and maxSize to keep a region usable. Because groups nest, a horizontal split can contain a vertical split for full IDE-style layouts.
Why use it
- Real drag-to-resize: react-resizable-panels handles pointer dragging, percentage math and constraints, so panels resize smoothly and stay within their min and max bounds.
- Horizontal, vertical and nested: Set direction on the group and nest groups inside panels to build side-by-side, stacked or full editor-style layouts from the same three parts.
- Optional visible grip: The whole seam is draggable, and withHandle adds a centered grip so the affordance is obvious. The grip auto-rotates for vertical groups.
- Keyboard accessible: The underlying resize handle is focusable and moves with the arrow keys, and it carries a focus-visible ring so keyboard users can see and drive it.
- Agent-readable metadata: The registry item describes its parts and intended use in plain words, so an MCP agent can find, inspect and install it without guessing.
Features
- shadcn registry install: One command adds the component, react-resizable-panels and the @ai2/tokens theme to your project.
- Percentage sizing: defaultSize, minSize and maxSize are percentages of the group, so layouts stay proportional as the container grows or shrinks.
- Direction-aware handle: The handle reads data-panel-group-direction to switch between a vertical rule and a horizontal rule, and rotates the grip to match.
- Persisted layouts: Pass autoSaveId to a group and react-resizable-panels stores the sizes, restoring the user's layout on the next visit.
- Collapsible panels: Mark a panel collapsible to let it snap shut past its minSize, useful for sidebars that fold away.
- Data attributes for styling: Every part exposes data-slot, and the group and handle expose data-panel-group-direction, so you can restyle seams from CSS without forking.
Production tips
- Give the group a height: Panels fill their group, so a horizontal group still needs a height (for example h-48 or h-full). Without one the panels collapse to zero.
- Make sizes add up to 100: defaultSize values across one group are percentages; keep them summing to 100 so the initial layout matches what you intend.
- Set a minSize on tight panels: A sidebar dragged to a few pixels is unusable. Give small panels a minSize so they stay legible no matter how far the handle is pulled.
- Use withHandle for discoverability: The bare seam works but is easy to miss. Add withHandle on the primary dividers so users see there is something to drag.
- Nest, do not overcrowd: Two or three panels per group reads clearly. For an IDE layout, nest a vertical group inside a panel rather than packing five panels into one row.
Works with the rest of ai2
Resizable panels frame larger layouts across the registry. Fill a panel with a ai2 Scroll Area so long content scrolls inside its region, and drop an ai2 Item list into a sidebar panel for a navigable file tree.
Pair it with a ai2 Card inside each panel for framed regions, or a ai2 Separator for non-draggable divisions within a single panel. Everything shares one token source, so combinations stay visually consistent in both modes.