VibeUI
ComponentsDocs
VibeUI

Components with taste, that you own. Copy the source, retint the tokens, keep every line.

Library

  • All components
  • Signature pieces
  • Text effects
  • Footers

Docs

  • Introduction
  • Installation
  • Theming
  • CLI

Project

  • GitHub
  • Agents and MCP
  • Issues
  • MIT license
© 2026 VibeUI · MIT licensed
  1. Components
  2. Data & surfaces
  3. Resizable

Resizable

Split panels with draggable dividers, laid out side by side or stacked.

Data & surfacesClient component

Files

  • button.tsx
  • dialog.tsx
  • dock.tsx
  • drawer.tsx
  • tokens.css

Editor

export function Button() {
 return <button className="…" />
}

Terminal

$ npx @agents-npm-packages/vibeui add drawer

✓ drawer → src/components/ui/drawer.tsx

Install

bash
npx @agents-npm-packages/vibeui add resizable --dir ./src/components/ui

Import

tsx
import { Resizable } from "@/components/ui/resizable"

API

Generated from the component's TypeScript types, so it always matches the source. * marks a required prop.

<Resizable>

also accepts every HTMLAttributes<HTMLDivElement> attribute

PropTypeDefaultDescription
children*ReactNode—One child per panel.
direction"horizontal" | "vertical""horizontal"`horizontal` puts panels side by side, `vertical` stacks them (give the container a height).
sizesnumber[]—Controlled panel sizes as percentages that add up to 100.
defaultSizesnumber[]—Initial sizes when uncontrolled. Defaults to equal panels.
minSizenumber | number[]12Smallest size any panel can shrink to, as a percentage. One number, or one per panel.
onSizesChange((sizes: number[]) => void)—Called while a divider moves.

Source

src/components/ui/resizable.tsx
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";

/**
 * Split panels with draggable dividers, laid out side by side or stacked.
 *
 * Each child is one panel. The dividers are keyboard-friendly separators: arrows nudge, Home and End jump to the limits.
 */
export function Resizable({
  children,
  direction = "horizontal",
  sizes: controlled,
  defaultSizes,
  minSize = 12,
  onSizesChange,
  className,
  ...props
}: Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & {
  /** One child per panel. */
  children: React.ReactNode;
  /** `horizontal` puts panels side by side, `vertical` stacks them (give the container a height). */
  direction?: "horizontal" | "vertical";
  /** Controlled panel sizes as percentages that add up to 100. */
  sizes?: number[];
  /** Initial sizes when uncontrolled. Defaults to equal panels. */
  defaultSizes?: number[];
  /** Smallest size any panel can shrink to, as a percentage. One number, or one per panel. */
  minSize?: number | number[];
  /** Called while a divider moves. */
  onSizesChange?: (sizes: number[]) => void;
}) {
  const panels = React.Children.toArray(children);
  const n = panels.length;
  const root = React.useRef<HTMLDivElement>(null);
  const [inner, setInner] = React.useState<number[]>(
    () => defaultSizes ?? Array.from({ length: n }, () => 100 / n),
  );
  const sizes = controlled ?? inner;
  const [active, setActive] = React.useState<number | null>(null);
  const drag = React.useRef<{ at: number; start: number; a: number; b: number } | null>(null);
  const horizontal = direction === "horizontal";

  const min = (i: number) => (Array.isArray(minSize) ? (minSize[i] ?? 10) : minSize);

  const apply = (i: number, nextA: number, a0: number, b0: number) => {
    const total = a0 + b0;
    const a = Math.max(min(i), Math.min(total - min(i + 1), nextA));
    const next = sizes.slice();
    next[i] = a;
    next[i + 1] = total - a;
    if (controlled === undefined) setInner(next);
    onSizesChange?.(next);
  };

  const onPointerDown = (e: React.PointerEvent<HTMLDivElement>, i: number) => {
    if (e.button !== 0) return;
    e.currentTarget.setPointerCapture(e.pointerId);
    drag.current = {
      at: i,
      start: horizontal ? e.clientX : e.clientY,
      a: sizes[i],
      b: sizes[i + 1],
    };
    setActive(i);
  };

  const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
    const d = drag.current;
    const el = root.current;
    if (!d || !el) return;
    const rect = el.getBoundingClientRect();
    const px = (horizontal ? e.clientX : e.clientY) - d.start;
    const size = horizontal ? rect.width : rect.height;
    apply(d.at, d.a + (px / size) * 100, d.a, d.b);
  };

  const onPointerUp = () => {
    drag.current = null;
    setActive(null);
  };

  const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>, i: number) => {
    const grow = horizontal ? "ArrowRight" : "ArrowDown";
    const shrink = horizontal ? "ArrowLeft" : "ArrowUp";
    const step = e.shiftKey ? 10 : 4;
    const a0 = sizes[i];
    const b0 = sizes[i + 1];
    let next: number | null = null;
    if (e.key === grow) next = a0 + step;
    else if (e.key === shrink) next = a0 - step;
    else if (e.key === "Home") next = 0;
    else if (e.key === "End") next = 100;
    if (next === null) return;
    e.preventDefault();
    apply(i, next, a0, b0);
  };

  return (
    <div
      {...props}
      ref={root}
      className={cn(
        "flex w-full overflow-hidden",
        "h-full",
        horizontal ? "flex-row" : "flex-col",
        active !== null && "select-none",
        className,
      )}
    >
      {panels.map((panel, i) => (
        <React.Fragment key={i}>
          <div
            className="min-h-0 min-w-0 overflow-auto"
            style={{
              flex: `0 0 ${sizes[i]}%`,
              transition: active === null ? "flex-basis 0.2s var(--ease-out)" : undefined,
            }}
          >
            {panel}
          </div>
          {i < n - 1 && (
            <div
              role="separator"
              tabIndex={0}
              aria-orientation={horizontal ? "vertical" : "horizontal"}
              aria-label={`Resize panel ${i + 1} and ${i + 2}`}
              aria-valuenow={Math.round(sizes[i])}
              aria-valuemin={Math.round(min(i))}
              aria-valuemax={Math.round(sizes[i] + sizes[i + 1] - min(i + 1))}
              onPointerDown={(e) => onPointerDown(e, i)}
              onPointerMove={onPointerMove}
              onPointerUp={onPointerUp}
              onPointerCancel={onPointerUp}
              onKeyDown={(e) => onKeyDown(e, i)}
              className={cn(
                "group relative z-10 flex shrink-0 touch-none items-center justify-center outline-none",
                horizontal ? "w-3 cursor-col-resize" : "h-3 cursor-row-resize",
              )}
            >
              <span
                aria-hidden
                className={cn(
                  "absolute bg-line-strong transition-colors group-hover:bg-ink/40 group-focus-visible:bg-ink group-active:bg-ink",
                  horizontal
                    ? "inset-y-0 left-1/2 w-px -translate-x-1/2"
                    : "inset-x-0 top-1/2 h-px -translate-y-1/2",
                )}
              />
              <span
                aria-hidden
                className={cn(
                  "relative rounded-full bg-line-strong shadow-quiet transition-[background-color,transform] group-hover:bg-ink/50 group-focus-visible:bg-ink group-active:scale-110 group-active:bg-ink",
                  horizontal ? "h-8 w-1" : "h-1 w-8",
                )}
              />
            </div>
          )}
        </React.Fragment>
      ))}
    </div>
  );
}

Exports Resizable. Peer dependencies: clsx, tailwind-merge.

PreviousListNextScroll Area