suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Components82
  • Overview
  • Accordion
  • Alert
  • Alert Dialog
  • Alert Sheet
  • Amount Input
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Bottom Bar
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Chip
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • FAB
  • Field
  • Fullscreen Menu
  • Haptics
  • Header
  • Hover Card
  • Icon
  • Input
  • Input Accessory Bar
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Keyboard Scroll View
  • Keypad
  • Label
  • Marker
  • Menubar
  • Message
  • Message Scroller
  • Native Select
  • Navigation Menu
  • Page Indicator
  • Pagination
  • Pin Pad
  • Popover
  • Progress
  • QR Scanner
  • Questionnaire
  • Radio Group
  • Resizable
  • Screen
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Signature Pad
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Tab Bar
  • Table
  • Tabs
  • Text
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip
  • Wheel Picker
  • Widget

suiss/ui · Component

Resizable

Panels separated by draggable handles that let users resize sections of a layout.

Resizable is an open-source component from suiss/ui for React. The @esuiss/ui CLI copies its source into your project, so you own the code.

import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from "@/components/ui/resizable"

function Pane({ label }: { label: string }) {
  return (
    <div className="flex h-full items-center justify-center p-6 text-sm font-medium">
      {label}
    </div>
  )
}

export default function ResizableDemo() {
  return (
    <div className="h-80 w-[36rem] overflow-hidden rounded-2xl border">
      <ResizablePanelGroup orientation="horizontal">
        <ResizablePanel defaultSize="50%">
          <Pane label="Inbox" />
        </ResizablePanel>
        <ResizableHandle />
        <ResizablePanel defaultSize="50%">
          <Pane label="Message" />
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  )
}

Resizable wraps react-resizable-panels. ResizablePanelGroup lays out its children in a row or column, alternating ResizablePanel and ResizableHandle. Drag a handle, or focus it and use the keyboard, to change the size of the panels on either side. Groups can be nested to build multi-pane layouts such as an editor with a sidebar and a terminal.

The cards variant renders each panel as a rounded card with a pill-shaped grip between them. Use Resizable for user-adjustable layouts; use Sidebar for a standard collapsible app navigation.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/resizable

Works in any project with a components.json. See Installation for the one-line registry setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add resizable

Manual

  1. Install the dependencies and add the cn helper:
$ pnpm add react-resizable-panels
  1. Copy the source into components/ui/resizable.tsx:
components/ui/resizable.tsx
"use client"

import * as React from "react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"

function animateJumps(
  group: HTMLElement | null,
  previous: ResizablePrimitive.Layout | null,
  next: ResizablePrimitive.Layout
) {
  if (!group || !previous) return
  if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return
  const jumped = Object.entries(next).some(
    ([id, size]) => Math.abs(size - (previous[id] ?? size)) > 8
  )
  1. Update the import paths to match your project.

Usage

import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable"
<ResizablePanelGroup orientation="horizontal">
  <ResizablePanel defaultSize="50%">
    <Pane label="Inbox" />
  </ResizablePanel>
  <ResizableHandle />
  <ResizablePanel defaultSize="50%">
    <Pane label="Message" />
  </ResizablePanel>
</ResizablePanelGroup>

Set orientation on the group to horizontal (default) or vertical. Size panels with defaultSize, minSize and maxSize, for example "30%". Make a panel collapsible with a collapsedSize to let it collapse when dragged past its minimum.

Use onLayoutChange on the group to read the current sizes, keyed by each panel's id.

Examples

With handle

A sidebar and content split with a visible grip and a minimum sidebar size.

import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from "@/components/ui/resizable"

function Pane({ label }: { label: string }) {
  return (
    <div className="flex h-full items-center justify-center p-6 text-sm font-medium">
      {label}
    </div>
  )
}

export default function ResizableWithHandle() {
  return (
    <div className="h-80 w-[36rem] overflow-hidden rounded-2xl border">
      <ResizablePanelGroup orientation="horizontal">
        <ResizablePanel defaultSize="30%" minSize="20%">
          <Pane label="Sidebar" />
        </ResizablePanel>
        <ResizableHandle withHandle />
        <ResizablePanel defaultSize="70%">
          <Pane label="Content" />
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  )
}

Vertical

Panels stacked vertically and resized with the up and down arrow keys.

import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from "@/components/ui/resizable"

function Pane({ label }: { label: string }) {
  return (
    <div className="flex h-full items-center justify-center p-6 text-sm font-medium">
      {label}
    </div>
  )
}

export default function ResizableVertical() {
  return (
    <div className="h-80 w-[36rem] overflow-hidden rounded-2xl border">
      <ResizablePanelGroup orientation="vertical">
        <ResizablePanel defaultSize="30%" minSize="20%">
          <Pane label="Sidebar" />
        </ResizablePanel>
        <ResizableHandle withHandle />
        <ResizablePanel defaultSize="70%">
          <Pane label="Content" />
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  )
}

Nested

A vertical group nested inside a horizontal group to create three panes.

import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from "@/components/ui/resizable"

function Pane({ label }: { label: string }) {
  return (
    <div className="flex h-full items-center justify-center p-6 text-sm font-medium">
      {label}
    </div>
  )
}

export default function ResizableNested() {
  return (
    <div className="h-80 w-[36rem] overflow-hidden rounded-2xl border">
      <ResizablePanelGroup orientation="horizontal">
        <ResizablePanel defaultSize="30%" minSize="20%">
          <Pane label="Files" />
        </ResizablePanel>
        <ResizableHandle withHandle />
        <ResizablePanel defaultSize="70%">
          <ResizablePanelGroup orientation="vertical">
            <ResizablePanel defaultSize="65%">
              <Pane label="Editor" />
            </ResizablePanel>
            <ResizableHandle withHandle />
            <ResizablePanel defaultSize="35%">
              <Pane label="Terminal" />
            </ResizablePanel>
          </ResizablePanelGroup>
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  )
}

Cards

The cards variant with two collapsible panels that report their share of the space.

import { CashRegisterIcon, CookingPotIcon } from "@phosphor-icons/react"
import * as React from "react"
import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from "@/components/ui/resizable"
import { ScrollArea } from "@/components/ui/scroll-area"

function CardPane({
  icon,
  label,
  size,
}: {
  icon: React.ReactNode
  label: string
  size: number | undefined
}) {
  return (
    <div className="size-full p-2">
      <ScrollArea className="size-full">
        <div className="flex min-h-full flex-col items-center justify-center gap-3 p-3 text-center">
          <span className="flex size-13 shrink-0 items-center justify-center rounded-2xl bg-secondary text-secondary-foreground [&_svg]:size-6">
            {icon}
          </span>
          <div className="flex flex-col gap-0.5">
            <p className="text-sm font-semibold">{label}</p>
            <p className="text-sm text-muted-foreground tabular-nums">
              {size === undefined ? "" : `${Math.round(size)}%`}
            </p>
          </div>
        </div>
      </ScrollArea>
    </div>
  )
}

function CardsExample(props: React.ComponentProps<typeof ResizablePanelGroup>) {
  const [layout, setLayout] = React.useState<Record<string, number>>({})

  return (
    <ResizablePanelGroup {...props} variant="cards" onLayoutChange={setLayout}>
      <ResizablePanel
        id="pos"
        defaultSize="50%"
        minSize="25%"
        collapsible
        collapsedSize="0%"
        collapsedThreshold="1%"
      >
        <CardPane
          icon={
            <CashRegisterIcon />
          }
          label="Point of sale"
          size={layout.pos}
        />
      </ResizablePanel>
      <ResizableHandle />
      <ResizablePanel
        id="kitchen"
        defaultSize="50%"
        minSize="25%"
        collapsible
        collapsedSize="0%"
        collapsedThreshold="1%"
      >
        <CardPane
          icon={
            <CookingPotIcon />
          }
          label="Kitchen display"
          size={layout.kitchen}
        />
      </ResizablePanel>
    </ResizablePanelGroup>
  )
}

export default function ResizableCards() {
  return (
    <div className="h-80 w-[36rem]">
      <CardsExample orientation="horizontal" />
    </div>
  )
}

Cards vertical

The cards variant in a vertical orientation.

import { CashRegisterIcon, CookingPotIcon } from "@phosphor-icons/react"
import * as React from "react"
import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from "@/components/ui/resizable"
import { ScrollArea } from "@/components/ui/scroll-area"

function CardPane({
  icon,
  label,
  size,
}: {
  icon: React.ReactNode
  label: string
  size: number | undefined
}) {
  return (
    <div className="size-full p-2">
      <ScrollArea className="size-full">
        <div className="flex min-h-full flex-col items-center justify-center gap-3 p-3 text-center">
          <span className="flex size-13 shrink-0 items-center justify-center rounded-2xl bg-secondary text-secondary-foreground [&_svg]:size-6">
            {icon}
          </span>
          <div className="flex flex-col gap-0.5">
            <p className="text-sm font-semibold">{label}</p>
            <p className="text-sm text-muted-foreground tabular-nums">
              {size === undefined ? "" : `${Math.round(size)}%`}
            </p>
          </div>
        </div>
      </ScrollArea>
    </div>
  )
}

function CardsExample(props: React.ComponentProps<typeof ResizablePanelGroup>) {
  const [layout, setLayout] = React.useState<Record<string, number>>({})

  return (
    <ResizablePanelGroup {...props} variant="cards" onLayoutChange={setLayout}>
      <ResizablePanel
        id="pos"
        defaultSize="50%"
        minSize="25%"
        collapsible
        collapsedSize="0%"
        collapsedThreshold="1%"
      >
        <CardPane
          icon={
            <CashRegisterIcon />
          }
          label="Point of sale"
          size={layout.pos}
        />
      </ResizablePanel>
      <ResizableHandle />
      <ResizablePanel
        id="kitchen"
        defaultSize="50%"
        minSize="25%"
        collapsible
        collapsedSize="0%"
        collapsedThreshold="1%"
      >
        <CardPane
          icon={
            <CookingPotIcon />
          }
          label="Kitchen display"
          size={layout.kitchen}
        />
      </ResizablePanel>
    </ResizablePanelGroup>
  )
}

export default function ResizableCardsVertical() {
  return (
    <div className="h-80 w-[36rem]">
      <CardsExample orientation="vertical" />
    </div>
  )
}

API Reference

Anatomy3 parts
<ResizablePanelGroup>
<ResizablePanel />
<ResizableHandle />
</ResizablePanelGroup>
01

ResizablePanelGroup

The container that arranges panels and handles and tracks the layout.

PropTypeDefaultDescription
variant"default" | "cards""default"Sets the visual style: default for flat panels with thin dividers, or cards for rounded panels with pill grips.

Also accepts the props of ResizablePrimitive.GroupProps.

02

ResizablePanel

One resizable section of the layout.

Also accepts the props of ResizablePrimitive.PanelProps.

03

ResizableHandle

The draggable divider between two panels.

PropTypeDefaultDescription
withHandleboolean-Shows a visible grip on the divider.

Also accepts the props of ResizablePrimitive.SeparatorProps.

Accessibility

  • Each handle has the separator role with aria-valuenow and aria-orientation, and is focusable with Tab.
  • Arrow keys resize the adjacent panels; Home and End move the handle to its minimum and maximum.
  • A panel that collapses to zero width or height becomes inert, so its content leaves the tab order.

Notes

  • Styling hooks: data-slot values resizable-panel-group, resizable-panel and resizable-handle. The group exposes data-variant, and collapsed panels expose data-empty.
  • A handle next to a collapsed panel gets data-collapsed set to start or end, and its grip appears on hover.
  • Large layout jumps, such as collapsing or expanding a panel, animate unless the user prefers reduced motion.
  • Uses the border, muted and muted-foreground colors.
NextScreenPreviousRadio Group

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • With handle
    • Vertical
    • Nested
    • Cards
    • Cards vertical
  4. 04API Reference
    • ResizablePanelGroup
    • ResizablePanel
    • ResizableHandle
  5. 05Accessibility
  6. 06Notes