Skip to content
Components

Resizable Panels

Resizable Panels for a useful, keyboard-friendly product interface.

Open ↗

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.json

Use 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 →