HextaUI

Resizable

Paneles que puedes separar arrastrando, con un divisor discreto que despierta al pasar el cursor, tamaños que se deslizan al restablecer o colapsar, y diseños que persisten.

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

Añade el componente, los tokens del tema de HextaUI y los componentes de HextaUI de los que depende.

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

Los números son píxeles y las cadenas son porcentajes, así que defaultSize="30%" y minSize={240} funcionan. El divisor sigue siendo una línea fina hasta que lo alcanzas. Mientras arrastras, los paneles siguen exactamente al puntero; cualquier otro cambio se desliza, incluidos los restablecimientos con doble clic, los pasos con teclado y el colapso desde código.

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

Collapsible

Un panel collapsible se cierra de golpe al arrastrarlo por debajo de su minSize. Alterna desde un botón con usePanelRef; el colapso se anima.

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

Indicador de tamaño

showSize muestra el tamaño del panel anterior al divisor mientras arrastras o usas el teclado, en "percent" o "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>
  )
}

Recordar el diseño

useDefaultLayout guarda los tamaños en localStorage y los restaura en la siguiente visita sin animar la entrada. Dale un id a cada panel.

"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" apila los paneles. Los grupos se pueden anidar, como en la demo de arriba.

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

Grip visible

withHandle mantiene el grip visible en lugar de revelarlo al pasar el cursor.

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

De derecha a izquierda

Los paneles y las flechas siguen la dirección de lectura.

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>
  )
}
KeyAcción
←→↑↓Mueve el divisor enfocado un 5% en la dirección de su grupo.
HomeEndMueve el divisor hasta donde lo permitan los paneles.
EnterColapsa o expande un panel colapsable.
F6Pasa al siguiente divisor del grupo; Shift vuelve atrás.
  • Cada divisor es un separator enfocable que informa del tamaño del panel anterior.
  • El indicador de tamaño es decorativo; los lectores de pantalla reciben el valor propio del separator.
  • Con movimiento reducido, los tamaños cambian al instante y el divisor no se anima.
PropTipoPredeterminado
orientation
"horizontal" | "vertical""horizontal"
defaultLayout
Layout–
onLayoutChangeCada cambio, incluido mientras se arrastra.
(layout) => void–
onLayoutChangedUna vez que se asienta un cambio.
(layout, meta) => void–
disabled
booleanfalse
groupRef
Ref<GroupImperativeHandle>–
AtributoDescripción
data-slot="resizable-panel-group"El grupo.
data-readySe define tras el montaje; a partir de entonces los tamaños se animan.
PropTipoPredeterminado
defaultSize
number | string–
minSize
number | string–
maxSize
number | string–
collapsible
booleanfalse
collapsedSize
number | string0
onResize
(size, id, previous) => void–
panelRef
Ref<PanelImperativeHandle>–
idNecesario para recordar los diseños.
string–
PropTipoPredeterminado
withHandleMuestra siempre el grip.
booleanfalse
showSize
boolean | "percent" | "pixels"false
disableDoubleClickEvita que el doble clic restablezca los paneles.
booleanfalse
disabled
booleanfalse
AtributoDescripción
data-slot="resizable-handle"El divisor.
data-separator"inactive", "hover", "active", "focus" o "disabled".
data-slot="resizable-handle-grip"La píldora del grip.

usePanelRef y useGroupRef te dan collapse, expand, resize y setLayout. useDefaultLayout persiste el diseño de un grupo.

Usado en bloques

Bloques que se construyen sobre Resizable.