90 / Blocks
Resizable Panels
Two panels and something to drag between them, where the divider is a real separator that also works from the keyboard.
index.tsx
Drag the divider, or put focus on it and use the arrow keys.
"use client"
import { ResizablePanels } from "mischief-ui/resizable-panels"
export function ResizablePanelsDemo() {
return (
<ResizablePanels
className="border-border h-56 w-full max-w-xl rounded-[var(--radius)] border"
defaultSize={38}
first={
<nav className="p-4">
<p className="text-muted-foreground text-xs font-semibold tracking-widest uppercase">
Files
</p>
<ul className="mt-3 grid gap-2 text-sm">
<li>index.tsx</li>
<li>styles.css</li>
<li>readme.md</li>
</ul>
</nav>
}
second={
<div className="p-4">
<p className="text-sm font-semibold">index.tsx</p>
<p className="text-muted-foreground mt-2 text-sm">
Drag the divider, or put focus on it and use the arrow keys.
</p>
</div>
}
/>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/resizable-panelsimport { ResizablePanels } from "mischief-ui/resizable-panels"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react"import { cn } from "@/lib/utils" export type ResizablePanelsProps = Omit< React.HTMLAttributes<HTMLDivElement>, "children" | "onChange"> & { first: React.ReactNode second: React.ReactNode direction?: "horizontal" | "vertical" /** Percentage of the box given to the first panel. */ size?: numberUsage
export function Workspace() {
return (
<ResizablePanels
defaultSize={38}
first={<FileTree />}
second={<Editor />}
className="h-96 rounded-xl border"
/>
)
}A separator with a value
The divider is a separator with a current value, a minimum, and a maximum, and it can be focused. That is what makes the split adjustable by anyone who cannot drag: arrow keys move it a step at a time, and Home and End take it to either limit.
A one pixel line is also close to impossible to hit with a pointer, so the area that responds is considerably wider than the line that is drawn. The line stays thin and the target does not.
Panels that cannot be lost
The size is clamped between the minimum and maximum on every change, wherever it came from, so neither panel can be dragged down to nothing and become impossible to get back. Both panels scroll their own contents rather than pushing the split around.
API
firstReactNodeThe panel the size applies to.secondReactNodeThe panel that takes the rest.direction"horizontal" | "vertical"Which way they sit. Defaults to "horizontal".sizenumberControlled percentage for the first panel.defaultSizenumberUncontrolled starting percentage. Defaults to 50.onSizeChange(size: number) => voidThe new percentage.minnumberSmallest percentage. Defaults to 15.maxnumberLargest percentage. Defaults to 85.stepnumberPercentage points an arrow key moves. Defaults to 4.Accessibility
The divider is a separator with an orientation, a current value, and its limits, reachable in the tab order and driven by the arrow keys, Home, and End. Its grab area is far larger than the line it draws. Both panels are ordinary regions and their contents are reached in the order they are written.