HextaUI

Resizable

Panels, die du auseinanderziehen kannst, mit einem dezenten Trenner, der beim Hover aufwacht, Größen, die beim Zurücksetzen oder Einklappen gleiten, und Layouts, die erhalten bleiben.

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

Fügt die Komponente, die HextaUI-Theme-Tokens und alle HextaUI-Komponenten hinzu, von denen sie abhängt.

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

Zahlen sind Pixel und Strings sind Prozente, sodass defaultSize="30%" und minSize={240} beide funktionieren. Der Trenner bleibt eine feine Linie, bis du nach ihm greifst. Beim Ziehen folgen die Panels dem Zeiger exakt; jede andere Änderung gleitet, auch Doppelklick-Resets, Tastaturschritte und Einklappen aus dem Code.

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

Collapsible

Ein collapsible-Panel schnappt zu, wenn es unter seine minSize gezogen wird. Schalte es per Button mit usePanelRef um; das Einklappen wird animiert.

"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>
  )
}

Größenanzeige

showSize zeigt beim Ziehen oder Tastaturbedienen die Größe des Panels vor dem Trenner, in "percent" oder "pixels".

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>
  )
}

Layout merken

useDefaultLayout speichert Größen in localStorage und stellt sie beim nächsten Besuch ohne Animation wieder her. Gib jedem Panel eine id.

"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>
  )
}

Vertikal

orientation="vertical" stapelt Panels. Gruppen lassen sich verschachteln, wie in der Demo oben.

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>
  )
}

Sichtbarer Griff

withHandle zeigt den Griff dauerhaft, statt ihn erst bei Hover einzublenden.

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>
  )
}

Rechts nach links

Panels und Pfeiltasten folgen der Leserichtung.

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>
  )
}
TasteAktion
←→↑↓Bewegt den fokussierten Trenner um 5% entlang der Richtung seiner Gruppe.
HomeEndBewegt den Trenner so weit, wie die Panels es erlauben.
EnterKlappt ein einklappbares Panel ein oder aus.
F6Wechselt zum nächsten Trenner der Gruppe; Shift geht zurück.
  • Jeder Trenner ist ein fokussierbarer separator, der die Größe des Panels davor meldet.
  • Die Größenanzeige ist Dekoration; Screenreader erhalten den eigenen Wert des Separators.
  • Bei reduzierter Bewegung ändern sich Größen sofort, und der Trenner animiert nicht.
PropTypStandard
orientation
"horizontal" | "vertical""horizontal"
defaultLayout
Layout–
onLayoutChangeJede Änderung, auch während des Ziehens.
(layout) => void–
onLayoutChangedSobald eine Änderung zur Ruhe kommt.
(layout, meta) => void–
disabled
booleanfalse
groupRef
Ref<GroupImperativeHandle>–
AttributBeschreibung
data-slot="resizable-panel-group"Die Gruppe.
data-readyWird nach dem Mount gesetzt; Größen animieren ab dann.
PropTypStandard
defaultSize
number | string–
minSize
number | string–
maxSize
number | string–
collapsible
booleanfalse
collapsedSize
number | string0
onResize
(size, id, previous) => void–
panelRef
Ref<PanelImperativeHandle>–
idNötig, um Layouts zu merken.
string–
PropTypStandard
withHandleDen Griff immer anzeigen.
booleanfalse
showSize
boolean | "percent" | "pixels"false
disableDoubleClickVerhindere, dass ein Doppelklick die Panels zurücksetzt.
booleanfalse
disabled
booleanfalse
AttributBeschreibung
data-slot="resizable-handle"Der Trenner.
data-separator"inactive", "hover", "active", "focus" oder "disabled".
data-slot="resizable-handle-grip"Die Griff-Pille.

usePanelRef und useGroupRef geben dir collapse, expand, resize und setLayout. useDefaultLayout persistiert das Layout einer Gruppe.

In Blocks verwendet

Blocks, die auf Resizable aufbauen.