GitHub

Resizable

Panel layout where panels can be resized by dragging handles between them. Uses compound component API for flexible layouts.

Drag to ResizeCompound APIMin/Max BoundsHorizontal Layout

Install

$npx react-principles add resizable
01

Live Demo

Explore all variants and interactive states in Storybook.

Open Storybookopen_in_new

Basic Horizontal Layout

Left Panel

Drag the handle to resize

Right Panel

Panels resize smoothly


Code Editor Layout

File Explorer

src/
components/
utils/
index.tsx

Code Editor

import React from 'react';
export default function App() {
return <div>Hello</div>;
}
02

Code Snippet

src/ui/Resizable.tsx
import { Resizable } from "@/ui/Resizable";

// Horizontal layout
<Resizable direction="horizontal" className="h-96">
  <Resizable.Panel defaultSize={50}>
    <div>Left panel</div>
  </Resizable.Panel>
  <Resizable.Handle />
  <Resizable.Panel defaultSize={50}>
    <div>Right panel</div>
  </Resizable.Panel>
</Resizable>

// With visual grip
<Resizable direction="horizontal" className="h-96">
  <Resizable.Panel defaultSize={40}>
    <div>Sidebar</div>
  </Resizable.Panel>
  <Resizable.Handle withHandle />
  <Resizable.Panel defaultSize={60}>
    <div>Main content</div>
  </Resizable.Panel>
</Resizable>

// Three panels
<Resizable direction="horizontal" className="h-96">
  <Resizable.Panel defaultSize={33.33}>
    <div>Panel 1</div>
  </Resizable.Panel>
  <Resizable.Handle />
  <Resizable.Panel defaultSize={33.33}>
    <div>Panel 2</div>
  </Resizable.Panel>
  <Resizable.Handle />
  <Resizable.Panel defaultSize={33.34}>
    <div>Panel 3</div>
  </Resizable.Panel>
</Resizable>
03

Copy-Paste (Single File)

Resizable.tsx
import { cn } from "@/lib/utils";
import React, { useRef, useState, type ReactNode } from "react";

// ─── Types ────────────────────────────────────────────────────────────────────

export type ResizableDirection = "horizontal" | "vertical";

export interface ResizableProps {
  direction: ResizableDirection;
  children: ReactNode;
  className?: string;
}

export interface ResizablePanelProps {
  defaultSize?: number;
  minSize?: number;
  maxSize?: number;
  children: ReactNode;
  className?: string;
}

export interface ResizableHandleProps {
  withHandle?: boolean;
  disabled?: boolean;
  className?: string;
}

// ─── Main Component ───────────────────────────────────────────────────────────

export function Resizable({ direction, children, className }: ResizableProps) {
  const [sizes, setSizes] = useState<number[]>([]);
  const [activeHandle, setActiveHandle] = useState<number | null>(null);
  const containerRef = useRef<HTMLDivElement>(null);

  // Count panels and assign indices
  let panelCount = 0;
  const childrenWithProps = React.Children.map(children, (child) => {
    if (!React.isValidElement(child)) return child;

    if (child.type === Resizable.Panel) {
      const index = panelCount++;

      // Initialize size
      if (sizes[index] === undefined) {
        setSizes((prev) => {
          const next = [...prev];
          next[index] = (child.props as ResizablePanelProps).defaultSize || 50;
          return next;
        });
      }

      return React.cloneElement(child, {
        index,
        sizes,
        setSizes,
        containerRef,
        direction,
        activeHandle,
        setActiveHandle,
      } as any);
    }

    if (child.type === Resizable.Handle) {
      const handleIndex = panelCount - 1;
      return React.cloneElement(child, {
        index: handleIndex,
        sizes,
        setSizes,
        containerRef,
        direction,
        activeHandle,
        setActiveHandle,
      } as any);
    }

    return child;
  });

  return (
    <div
      ref={containerRef}
      className={cn("flex", direction === "horizontal" ? "flex-row" : "flex-col", className)}
    >
      {childrenWithProps}
    </div>
  );
}

// ─── Panel Sub-Component ───────────────────────────────────────────────────────

Resizable.Panel = function ResizablePanel({
  defaultSize = 50,
  children,
  className,
  index,
  sizes,
}: ResizablePanelProps & {
  index?: number;
  sizes?: number[];
  setSizes?: any;
  containerRef?: any;
  direction?: ResizableDirection;
  activeHandle?: number | null;
  setActiveHandle?: any;
}) {
  const size = index !== undefined && sizes?.[index] !== undefined ? sizes[index] : defaultSize;

  return (
    <div
      className={cn("flex-shrink-0", className)}
      style={{ flexBasis: `${size}%`, flexShrink: 0 }}
    >
      {children}
    </div>
  );
};

// ─── Handle Sub-Component ───────────────────────────────────────────────────────────

Resizable.Handle = function ResizableHandle({
  withHandle = false,
  disabled = false,
  className,
  index,
  sizes,
  setSizes,
  containerRef,
  direction = "horizontal",
  activeHandle,
}: ResizableHandleProps & {
  index?: number;
  sizes?: number[];
  setSizes?: any;
  containerRef?: any;
  activeHandle?: number | null;
}) {
  const isHorizontal = direction === "horizontal";

  const handleMouseDown = (e: React.MouseEvent) => {
    if (disabled || index === undefined) return;
    e.preventDefault();

    const container = containerRef.current;
    if (!container || !setSizes) return;

    const startX = e.clientX;
    const initialSizes = [...(sizes || [])];

    setActiveHandle(index);

    const handleMouseMove = (moveEvent: MouseEvent) => {
      const containerWidth = container.offsetWidth;
      const deltaX = moveEvent.clientX - startX;
      const deltaPercent = (deltaX / containerWidth) * 100;

      if (index === undefined) return;

      const initialPanelSize = initialSizes[index] || 50;
      const newPanelSize = Math.max(10, Math.min(90, initialPanelSize + deltaPercent));
      const newNextPanelSize = initialSizes[index + 1] - (newPanelSize - initialPanelSize);

      if (newNextPanelSize >= 10 && newNextPanelSize <= 90) {
        setSizes((prev: number[]) => {
          const next = [...prev];
          next[index] = newPanelSize;
          next[index + 1] = newNextPanelSize;
          return next;
        });
      }
    };

    const handleMouseUp = () => {
      setActiveHandle(null);
      document.removeEventListener("mousemove", handleMouseMove);
      document.removeEventListener("mouseup", handleMouseUp);
    };

    document.addEventListener("mousemove", handleMouseMove);
    document.addEventListener("mouseup", handleMouseUp);
  };

  return (
    <div
      tabIndex={disabled ? -1 : 0}
      role="separator"
      aria-orientation={direction}
      className={cn(
        "flex-shrink-0 bg-slate-200 dark:bg-[#1f2937]",
        "hover:bg-primary/20",
        disabled && "opacity-50 cursor-not-allowed",
        !disabled && "select-none",
        !disabled && (isHorizontal ? "cursor-col-resize" : "cursor-row-resize"),
        isHorizontal ? "w-1" : "h-1",
        activeHandle === index && !disabled && "bg-primary",
        className
      )}
      onMouseDown={handleMouseDown}
    >
      {withHandle && (
        <div className={cn("flex items-center justify-center", isHorizontal ? "h-full w-4" : "w-full h-4")}>
          <div className={cn("bg-slate-400 dark:bg-slate-600 rounded-full", isHorizontal ? "w-1 h-8" : "w-8 h-1")} />
        </div>
      )}
    </div>
  );
};
04

Props

Resizable uses a compound component API with three components:

ComponentPropTypeDefaultDescription
Resizabledirection"horizontal" | "vertical"Layout direction. Currently only horizontal is fully implemented.
Resizable.PaneldefaultSizenumber50Initial size of panel as percentage (0-100).
Resizable.PanelminSizenumber10Minimum size as percentage. Panel won't shrink below this.
Resizable.PanelmaxSizenumber90Maximum size as percentage. Panel won't grow beyond this.
Resizable.HandlewithHandlebooleanfalseShow visual grip/handle on the drag separator.
Resizable.HandledisabledbooleanfalseDisable the handle and prevent resizing.
05

Usage Examples

Basic Two-Panel Layout

<Resizable direction="horizontal" className="h-96">
  <Resizable.Panel defaultSize={50}>
    <div>Left panel content</div>
  </Resizable.Panel>
  <Resizable.Handle withHandle />
  <Resizable.Panel defaultSize={50}>
    <div>Right panel content</div>
  </Resizable.Panel>
</Resizable>

Three-Panel Layout

<Resizable direction="horizontal" className="h-96">
  <Resizable.Panel defaultSize={33.33}>
    <div>Panel 1</div>
  </Resizable.Panel>
  <Resizable.Handle />
  <Resizable.Panel defaultSize={33.33}>
    <div>Panel 2</div>
  </Resizable.Panel>
  <Resizable.Handle />
  <Resizable.Panel defaultSize={33.34}>
    <div>Panel 3</div>
  </Resizable.Panel>
</Resizable>
React Principles