Resizable Panels
Resizable Panels for a useful, keyboard-friendly product interface.
Make it yours.
Install the editable source and its dependencies into your React + Tailwind project.
Scroll horizontally for long lines
pnpm dlx shadcn@4.0.8 add http://localhost:3000/r/resizable-panels.jsonUse it
Scroll horizontally for long lines
"use client";
import {ResizablePanels} from '@/components/jez-ui/ui/resizable-panels';
export default function Example(){
return <><div className="w-full max-w-2xl"><div className="mb-7"><h3 className="text-2xl tracking-tight">Space that works for you.</h3><p className="mt-2 text-sm leading-relaxed text-muted-foreground">Adjust the balance with a pointer or the arrow keys.</p></div><ResizablePanels left={<p className="text-sm">Drag the divider, or focus it and use arrow keys.</p>} right={<p className="text-sm">Room for your work.</p>}/></div></>;
}Props & types
Scroll horizontally for long lines
{
left: React.ReactNode;
right: React.ReactNode;
className?: string;
}Source
View 2 source files
registry/ui/resizable-panels.tsx
Scroll horizontally for long lines
"use client";
import * as React from "react";
import { cn } from "./utils";
export function ResizablePanels({
left,
right,
className,
}: {
left: React.ReactNode;
right: React.ReactNode;
className?: string;
}) {
const [width, setWidth] = React.useState(40);
const ref = React.useRef<HTMLDivElement>(null);
return (
<div
ref={ref}
className={cn(
"flex min-h-48 overflow-hidden rounded-xl border border-border",
className,
)}
>
<div style={{ width: width + "%" }} className="min-w-0 overflow-auto p-4">
{left}
</div>
<div
role="separator"
tabIndex={0}
aria-label="Resize panels"
aria-orientation="vertical"
aria-valuenow={width}
aria-valuemin={20}
aria-valuemax={80}
className="relative w-px shrink-0 cursor-col-resize touch-none bg-border after:absolute after:inset-y-0 after:-left-2 after:w-4 hover:bg-primary focus-visible:bg-primary"
onKeyDown={(e) => {
if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
e.preventDefault();
setWidth((w) =>
Math.max(20, Math.min(80, w + (e.key === "ArrowLeft" ? -5 : 5))),
);
}
}}
onPointerDown={(e) => e.currentTarget.setPointerCapture(e.pointerId)}
onPointerMove={(e) => {
if (e.currentTarget.hasPointerCapture(e.pointerId) && ref.current) {
const r = ref.current.getBoundingClientRect();
setWidth(
Math.max(
20,
Math.min(80, ((e.clientX - r.left) / r.width) * 100),
),
);
}
}}
onPointerUp={(e) => e.currentTarget.releasePointerCapture(e.pointerId)}
/>
<div className="min-w-0 flex-1 overflow-auto p-4">{right}</div>
</div>
);
}
registry/ui/utils.ts
Scroll horizontally for long lines
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...values: ClassValue[]) {
return twMerge(clsx(values));
}
Dependencies
clsx@2.1.1 · tailwind-merge@3.5.0
Accessibility & behaviour
Provide meaningful labels and preserve keyboard focus styles. Check contrast when customising theme colours.
Read the accessibility and performance guide →