HextaUI

Resizable

Panels you can drag apart, with a quiet divider that wakes up on hover, sizes that glide on reset or collapse, and layouts that persist.

ExplorerDrag the divider, or double-click it to reset.
EditorFocus a divider and use the arrow keys.
Terminal
import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from "@/components/ui/resizable"

function Pane({ title, children }: { title: string; children?: string }) {
  return (
    <div className="flex h-full flex-col gap-1 p-4">
      <span className="text-sm font-medium">{title}</span>
      <span className="text-sm text-muted-foreground">{children}</span>
    </div>
  )
}

export function ResizableDemo() {
  return (
    <div className="h-80 w-full max-w-2xl overflow-hidden rounded-xl border">
      <ResizablePanelGroup>
        <ResizablePanel defaultSize="28%" minSize="18%" maxSize="45%">
          <Pane title="Explorer">
            Drag the divider, or double-click it to reset.
          </Pane>
        </ResizablePanel>
        <ResizableHandle />
        <ResizablePanel>
          <ResizablePanelGroup orientation="vertical">
            <ResizablePanel defaultSize="65%" minSize="25%">
              <Pane title="Editor">
                Focus a divider and use the arrow keys.
              </Pane>
            </ResizablePanel>
            <ResizableHandle />
            <ResizablePanel minSize="15%">
              <Pane title="Terminal" />
            </ResizablePanel>
          </ResizablePanelGroup>
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/resizable.json

Adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from "@/components/ui/resizable"
<ResizablePanelGroup>
  <ResizablePanel defaultSize="30%" minSize="20%">Sidebar</ResizablePanel>
  <ResizableHandle />
  <ResizablePanel>Content</ResizablePanel>
</ResizablePanelGroup>

Numbers are pixels and strings are percentages, so defaultSize="30%" and minSize={240} both work. The divider stays a hairline until you reach for it. While you drag, panels follow the pointer exactly; every other change glides, including double-click resets, keyboard steps and collapsing from code.

ResizablePanelGroup
├── ResizablePanel
├── ResizableHandle
└── ResizablePanel

Collapsible

A collapsible panel snaps shut when dragged below its minSize. Toggle it from a button with usePanelRef; the collapse animates.

Drag the sidebar narrow to collapse it, or press Enter on the divider.
InboxDraftsArchive
Message
"use client"

import * as React from "react"
import { IconLayoutSidebar } from "@tabler/icons-react"

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

export function ResizableCollapsible() {
  const sidebar = usePanelRef()
  const [collapsed, setCollapsed] = React.useState(false)

  return (
    <div className="flex h-72 w-full max-w-2xl flex-col overflow-hidden rounded-xl border">
      <div className="flex items-center gap-2 border-b p-2">
        <Button
          variant="ghost"
          size="icon-sm"
          aria-label={collapsed ? "Show sidebar" : "Hide sidebar"}
          aria-pressed={!collapsed}
          onClick={() => {
            if (sidebar.current?.isCollapsed()) {
              sidebar.current.expand()
            } else {
              sidebar.current?.collapse()
            }
          }}
        >
          <IconLayoutSidebar />
        </Button>
        <span className="text-sm text-muted-foreground">
          Drag the sidebar narrow to collapse it, or press Enter on the divider.
        </span>
      </div>
      <div className="min-h-0 flex-1">
        <ResizablePanelGroup>
          <ResizablePanel
            panelRef={sidebar}
            collapsible
            defaultSize="30%"
            minSize="20%"
            collapsedSize="0%"
            onResize={(size) => setCollapsed(size.asPercentage === 0)}
          >
            <div className="flex h-full flex-col gap-1 p-4 text-sm">
              <span className="font-medium">Inbox</span>
              <span className="text-muted-foreground">Drafts</span>
              <span className="text-muted-foreground">Archive</span>
            </div>
          </ResizablePanel>
          <ResizableHandle />
          <ResizablePanel minSize="40%">
            <div className="flex h-full items-center justify-center text-sm font-medium">
              Message
            </div>
          </ResizablePanel>
        </ResizablePanelGroup>
      </div>
    </div>
  )
}

Size readout

showSize shows the panel before the divider while you drag or use the keyboard, in "percent" or "pixels".

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

export function ResizableSize() {
  return (
    <div className="h-56 w-full max-w-xl overflow-hidden rounded-xl border">
      <ResizablePanelGroup>
        <ResizablePanel defaultSize="320px" minSize="160px" maxSize="70%">
          <div className="flex h-full items-center justify-center text-sm font-medium">
            Canvas
          </div>
        </ResizablePanel>
        <ResizableHandle showSize="pixels" />
        <ResizablePanel minSize="20%">
          <div className="flex h-full items-center justify-center text-sm font-medium">
            Inspector
          </div>
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  )
}

Remember the layout

useDefaultLayout saves sizes to localStorage and restores them on the next visit without animating in. Give each panel an id.

Resize me, then reload
Detail
"use client"

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

export function ResizablePersist() {
  const { defaultLayout, onLayoutChanged } = useDefaultLayout({
    id: "hextaui-resizable-persist",
  })

  return (
    <div className="h-56 w-full max-w-md overflow-hidden rounded-xl border">
      <ResizablePanelGroup
        defaultLayout={defaultLayout}
        onLayoutChanged={onLayoutChanged}
      >
        <ResizablePanel id="list" defaultSize="40%" minSize="20%">
          <div className="flex h-full items-center justify-center p-4 text-center text-sm font-medium">
            Resize me, then reload
          </div>
        </ResizablePanel>
        <ResizableHandle />
        <ResizablePanel id="detail" minSize="20%">
          <div className="flex h-full items-center justify-center text-sm font-medium">
            Detail
          </div>
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  )
}

Vertical

orientation="vertical" stacks panels. Groups nest, as in the demo above.

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

export function ResizableVertical() {
  return (
    <div className="h-72 w-full max-w-md overflow-hidden rounded-xl border">
      <ResizablePanelGroup orientation="vertical">
        <ResizablePanel defaultSize="30%" minSize="20%">
          <div className="flex h-full items-center justify-center text-sm font-medium">
            Header
          </div>
        </ResizablePanel>
        <ResizableHandle />
        <ResizablePanel minSize="30%">
          <div className="flex h-full items-center justify-center text-sm font-medium">
            Content
          </div>
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  )
}

Visible grip

withHandle keeps the grip showing instead of revealing it on hover.

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

export function ResizableWithHandle() {
  return (
    <div className="h-56 w-full max-w-md overflow-hidden rounded-xl border">
      <ResizablePanelGroup>
        <ResizablePanel defaultSize="40%" minSize="20%">
          <div className="flex h-full items-center justify-center text-sm font-medium">
            Preview
          </div>
        </ResizablePanel>
        <ResizableHandle withHandle />
        <ResizablePanel minSize="20%">
          <div className="flex h-full items-center justify-center text-sm font-medium">
            Code
          </div>
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  )
}

Right to left

Panels and arrow keys follow the reading direction.

القائمة
المحتوى
import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from "@/components/ui/resizable"

export function ResizableRtl() {
  return (
    <div
      dir="rtl"
      className="h-56 w-full max-w-md overflow-hidden rounded-xl border"
    >
      <ResizablePanelGroup>
        <ResizablePanel defaultSize="35%" minSize="20%">
          <div className="flex h-full items-center justify-center text-sm font-medium">
            القائمة
          </div>
        </ResizablePanel>
        <ResizableHandle showSize />
        <ResizablePanel minSize="20%">
          <div className="flex h-full items-center justify-center text-sm font-medium">
            المحتوى
          </div>
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  )
}
KeyAction
←→↑↓Moves the focused divider by 5% along its group's direction.
HomeEndMoves the divider as far as the panels allow.
EnterCollapses or expands a collapsible panel.
F6Moves to the next divider in the group; Shift goes back.
  • Each divider is a focusable separator that reports the size of the panel before it.
  • The size readout is decorative; screen readers get the separator's own value.
  • With reduced motion, sizes change instantly and the divider doesn't animate.
PropTypeDefault
orientation
"horizontal" | "vertical""horizontal"
defaultLayout
Layout–
onLayoutChangeEvery change, including while dragging.
(layout) => void–
onLayoutChangedOnce a change settles.
(layout, meta) => void–
disabled
booleanfalse
groupRef
Ref<GroupImperativeHandle>–
AttributeDescription
data-slot="resizable-panel-group"The group.
data-readySet after mount; sizes animate from then on.
PropTypeDefault
defaultSize
number | string–
minSize
number | string–
maxSize
number | string–
collapsible
booleanfalse
collapsedSize
number | string0
onResize
(size, id, previous) => void–
panelRef
Ref<PanelImperativeHandle>–
idNeeded to remember layouts.
string–
PropTypeDefault
withHandleAlways show the grip.
booleanfalse
showSize
boolean | "percent" | "pixels"false
disableDoubleClickStop double-click from resetting the panels.
booleanfalse
disabled
booleanfalse
AttributeDescription
data-slot="resizable-handle"The divider.
data-separator"inactive", "hover", "active", "focus" or "disabled".
data-slot="resizable-handle-grip"The grip pill.

usePanelRef and useGroupRef give you collapse, expand, resize and setLayout. useDefaultLayout persists a group's layout.