Skip to content
Corsair UI
Browse the docs

ComponentsNo. 58 of 145

Resizable

Panels resized by dragging or with the keyboard, side by side or stacked.

Installation

pnpm dlx shadcn@latest add @corsair-ui/resizable

No setup needed: the CLI finds @corsair-ui in the shadcn registry directory. The files land in your project as source: edit them like your own code. More in Installation.

Also adds
utils
npm
clsxlucide-reactreact-resizable-panelstailwind-merge

Examples

Side by side

Three panels with grips; the handles also move with the arrow keys.

Files
Editor
Preview

Stacked

A vertical group inside a horizontal one.

Sidebar
Editor
Terminal

API

PropTypeDefault
orientation

ResizablePanelGroup: side by side or stacked.

"horizontal" | "vertical""horizontal"
defaultSize / minSize / maxSize

ResizablePanel: strings such as "30" are percentages, numbers are pixels.

string | number—
collapsible / collapsedSize

ResizablePanel: lets it shrink to collapsedSize.

boolean / string | number—
withHandle

ResizableHandle: a grip on the line, easier to find and grab.

booleanfalse
aria-label

ResizableHandle: name it after the panel it resizes when there are several.

string"Resize"

Built on react-resizable-panels; every prop it takes is passed through.

Styling

Restyle it in your project: className on each part, the theme variables, and these attributes with arbitrary variants, e.g. className="[&_[data-slot=resizable-handle]]:text-primary".

  • Each handle has data-separator ("inactive", "hover", "active", "focus", "disabled") and aria-orientation; the active one takes the ring colour.
  • withHandle adds the grip, resizable-handle-grip. The group has data-orientation.
data-slot
  • resizable-handle
  • resizable-handle-grip
  • resizable-panel
  • resizable-panel-group
State
  • data-orientationcomputed

react-resizable-panels also sets its own attributes on the parts, such as data-state and data-disabled.

Accessibility

  • Each handle is a focusable separator whose value is its position between the panels' limits.
  • The arrow keys resize (Left and Right side by side, Up and Down stacked), Home and End jump to the limits, Enter collapses a collapsible panel.
  • Sizes change instantly; nothing animates.

Compatibility

Tailwind CSS
3.4 and 4, both checked in CI
React
19
Rendering
Client component ("use client")

Source

1 file, installed as source you own
resizable.tsxView on GitHub
"use client";

import { GripVerticalIcon } from "lucide-react";
import type { Ref } from "react";
import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "@/registry/default/lib/utils";

interface ResizablePanelGroupProps extends Omit<ResizablePrimitive.GroupProps, "elementRef"> {
  /** "horizontal" puts the panels side by side, "vertical" stacks them. */
  orientation?: "horizontal" | "vertical";
  ref?: Ref<HTMLDivElement>;
}

/**
 * Panels whose sizes the user can change: a file tree beside an editor, a
 * preview under a form. Put `ResizablePanel`s in it with a
 * `ResizableHandle` between each pair. A handle is a focusable separator:
 * Tab reaches it, the arrow keys resize the panels on either side (Left and
 * Right side by side, Up and Down when stacked), Home and End move it to the
 * limits, Enter collapses or restores a collapsible panel, and screen readers
 * hear its position as a value between the panels' limits. Dragging with a
 * pointer works too. Sizes change instantly; nothing animates. Built on
 * `react-resizable-panels`: sizes such as `defaultSize="30"` are percentages,
 * plain numbers are pixels.
 *
 * @example
 * <ResizablePanelGroup orientation="horizontal" className="min-h-64 rounded-lg border">
 *   <ResizablePanel defaultSize="30" minSize="20">
 *     Charts
 *   </ResizablePanel>
 *   <ResizableHandle withHandle aria-label="Resize charts" />
 *   <ResizablePanel>Log</ResizablePanel>
 * </ResizablePanelGroup>
 */
function ResizablePanelGroup({
  className,
  orientation = "horizontal",
  style,
  ref,
  ...props
}: ResizablePanelGroupProps) {
  return (
    <ResizablePrimitive.Group
      data-slot="resizable-panel-group"
      data-orientation={orientation}
      orientation={orientation}
      elementRef={ref}
      className={cn("size-full", className)}
      // The library sets width and height to 100% inline, which would beat a
      // size given in className (h-64); size-full above is the same default.
      style={{ width: undefined, height: undefined, ...style }}
      {...props}
    />
  );
}

interface ResizablePanelProps extends Omit<ResizablePrimitive.PanelProps, "elementRef"> {
  ref?: Ref<HTMLDivElement>;
}

/**
 * One resizable area. Size it with `defaultSize`, `minSize` and `maxSize`;
 * `collapsible` lets it shrink to `collapsedSize`. Control it from code with
 * `panelRef` (`usePanelRef` from `react-resizable-panels`).
 */
function ResizablePanel({ ref, ...props }: ResizablePanelProps) {
  return <ResizablePrimitive.Panel data-slot="resizable-panel" elementRef={ref} {...props} />;
}

interface ResizableHandleProps extends Omit<ResizablePrimitive.SeparatorProps, "elementRef"> {
  /** Shows a grip on the line, so the handle is easier to find and to grab. */
  withHandle?: boolean;
  /** Accessible name of the separator. Name it after the panel it resizes when there are several. */
  "aria-label"?: string;
  ref?: Ref<HTMLDivElement>;
}

/**
 * The line between two panels that resizes them. Its state is in
 * `data-separator` ("inactive", "hover", "active", "focus", "disabled") and
 * its direction in `aria-orientation`, for your own styles.
 */
function ResizableHandle({
  withHandle = false,
  "aria-label": label = "Resize",
  className,
  ref,
  ...props
}: ResizableHandleProps) {
  return (
    <ResizablePrimitive.Separator
      data-slot="resizable-handle"
      aria-label={label}
      elementRef={ref}
      className={cn(
        "bg-border relative flex w-px shrink-0 items-center justify-center",
        // A wider invisible hit area centred on the 1px line.
        "after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2",
        "transition-[background-color,box-shadow] outline-none motion-reduce:transition-none",
        "focus-visible:ring-ring/50 focus-visible:ring-[3px]",
        "data-[separator=active]:bg-ring data-[separator=hover]:bg-ring/50",
        "aria-[disabled=true]:cursor-not-allowed aria-[disabled=true]:opacity-50",
        // aria-orientation="horizontal" is the line between stacked panels.
        "aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full",
        "aria-[orientation=horizontal]:after:inset-x-0 aria-[orientation=horizontal]:after:inset-y-auto aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2",
        "[&[aria-orientation=horizontal]>div]:rotate-90",
        className
      )}
      {...props}
    >
      {withHandle ? (
        <div
          data-slot="resizable-handle-grip"
          aria-hidden="true"
          className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-[4px] border"
        >
          <GripVerticalIcon className="size-2.5" />
        </div>
      ) : null}
    </ResizablePrimitive.Separator>
  );
}

export {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
  type ResizableHandleProps,
  type ResizablePanelGroupProps,
  type ResizablePanelProps,
};