HextaUI

Sheet

Un panel que se desliza desde cualquier borde, con deslizamiento para descartar, bloqueo de scroll y anidamiento apilado.

import { Button } from "@/components/ui/button"
import {
  Sheet,
  SheetBody,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

export function SheetDemo() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Edit profile
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Edit profile</SheetTitle>
          <SheetDescription>
            Changes are saved to your account when you click save.
          </SheetDescription>
        </SheetHeader>
        <SheetBody>
          <div className="flex flex-col gap-4">
            <div className="flex flex-col gap-1.5">
              <label htmlFor="sheet-demo-name" className="text-sm font-medium">
                Name
              </label>
              <input
                id="sheet-demo-name"
                defaultValue="Olivia Martin"
                className="h-9 rounded-md border border-input bg-transparent px-3 text-sm transition-shadow outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden pointer-coarse:text-touch"
              />
            </div>
            <div className="flex flex-col gap-1.5">
              <label
                htmlFor="sheet-demo-username"
                className="text-sm font-medium"
              >
                Username
              </label>
              <input
                id="sheet-demo-username"
                defaultValue="@olivia"
                className="h-9 rounded-md border border-input bg-transparent px-3 text-sm transition-shadow outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden pointer-coarse:text-touch"
              />
            </div>
          </div>
        </SheetBody>
        <SheetFooter>
          <SheetClose render={<Button variant="outline" />}>Cancel</SheetClose>
          <SheetClose render={<Button />}>Save changes</SheetClose>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/sheet.json

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

import {
  Sheet,
  SheetBody,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"
<Sheet>
  <SheetTrigger render={<Button variant="outline" />}>Open</SheetTrigger>
  <SheetContent>
    <SheetHeader>
      <SheetTitle>Edit profile</SheetTitle>
      <SheetDescription>Make changes to your profile.</SheetDescription>
    </SheetHeader>
    <SheetBody>{/* content */}</SheetBody>
    <SheetFooter>
      <SheetClose render={<Button />}>Save changes</SheetClose>
    </SheetFooter>
  </SheetContent>
</Sheet>
Sheet
├── SheetTrigger
└── SheetContent
    ├── SheetHeader
    │   ├── SheetTitle
    │   └── SheetDescription
    ├── SheetBody
    └── SheetFooter
        └── SheetClose

Lados

Define side para que entre desde cualquier borde. Cada sheet se puede deslizar de vuelta hacia su propio borde, y el gesto de cierre sigue tu dedo con inercia.

import { Button } from "@/components/ui/button"
import {
  Sheet,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

const sides = ["top", "right", "bottom", "left"] as const

export function SheetSides() {
  return (
    <div className="grid grid-cols-2 gap-2">
      {sides.map((side) => (
        <Sheet key={side}>
          <SheetTrigger render={<Button variant="outline" />}>
            {side}
          </SheetTrigger>
          <SheetContent side={side}>
            <SheetHeader>
              <SheetTitle>Opens from the {side}</SheetTitle>
              <SheetDescription>
                Swipe towards the {side} edge or press Esc to close it.
              </SheetDescription>
            </SheetHeader>
            <SheetFooter>
              <SheetClose render={<Button />}>Done</SheetClose>
            </SheetFooter>
          </SheetContent>
        </Sheet>
      ))}
    </div>
  )
}

Contenido largo

<SheetBody /> se desplaza por sí solo, así que el encabezado y el pie permanecen en su sitio por largo que sea el contenido.

import { Button } from "@/components/ui/button"
import {
  Sheet,
  SheetBody,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

export function SheetLongContent() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Release notes
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>What’s new</SheetTitle>
          <SheetDescription>Version 2.4.0</SheetDescription>
        </SheetHeader>
        <SheetBody>
          <div className="flex flex-col gap-3 text-sm text-muted-foreground">
            {Array.from({ length: 24 }, (_, index) => (
              <p key={index}>
                {index + 1}. Improved performance of the dashboard charts and
                fixed an issue where filters reset after navigation.
              </p>
            ))}
          </div>
        </SheetBody>
        <SheetFooter>
          <SheetClose render={<Button />}>Got it</SheetClose>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  )
}

Sin el botón de cierre

Define showCloseButton={false} cuando el pie ya tiene una forma de salir. Escape, el fondo y el deslizamiento siguen cerrándolo.

import { Button } from "@/components/ui/button"
import {
  Sheet,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

export function SheetNoCloseButton() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>Share</SheetTrigger>
      <SheetContent side="bottom" showCloseButton={false}>
        <SheetHeader>
          <SheetTitle>Share link</SheetTitle>
          <SheetDescription>
            Anyone with the link can view this document.
          </SheetDescription>
        </SheetHeader>
        <SheetFooter>
          <SheetClose render={<Button />}>Done</SheetClose>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  )
}

Anidado

Un sheet o alert dialog abierto desde un sheet se coloca encima. El padre se reduce ligeramente y cada capa adicional añade un fondo más claro, para que la pila siga siendo legible. Escape cierra solo la capa superior.

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import {
  Sheet,
  SheetBody,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

export function SheetNested() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Workspace settings
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Workspace settings</SheetTitle>
          <SheetDescription>
            Manage members and the danger zone.
          </SheetDescription>
        </SheetHeader>
        <SheetBody>
          <div className="flex flex-col items-start gap-2">
            <Sheet>
              <SheetTrigger render={<Button variant="outline" />}>
                Manage members
              </SheetTrigger>
              <SheetContent>
                <SheetHeader>
                  <SheetTitle>Members</SheetTitle>
                  <SheetDescription>
                    The parent sheet steps back while this one is open.
                  </SheetDescription>
                </SheetHeader>
              </SheetContent>
            </Sheet>
            <AlertDialog>
              <AlertDialogTrigger render={<Button variant="destructive" />}>
                Delete workspace
              </AlertDialogTrigger>
              <AlertDialogContent>
                <AlertDialogHeader>
                  <AlertDialogTitle>Delete workspace?</AlertDialogTitle>
                  <AlertDialogDescription>
                    All projects in this workspace will be removed.
                  </AlertDialogDescription>
                </AlertDialogHeader>
                <AlertDialogFooter>
                  <AlertDialogCancel>Cancel</AlertDialogCancel>
                  <AlertDialogAction variant="destructive">
                    Delete
                  </AlertDialogAction>
                </AlertDialogFooter>
              </AlertDialogContent>
            </AlertDialog>
          </div>
        </SheetBody>
      </SheetContent>
    </Sheet>
  )
}

Controlado

Pasa open y onOpenChange para abrirlo desde cualquier lugar, sin un trigger.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Sheet,
  SheetBody,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
} from "@/components/ui/sheet"

export function SheetControlled() {
  const [open, setOpen] = React.useState(false)

  return (
    <div className="flex flex-wrap items-center gap-3">
      <Button variant="outline" onClick={() => setOpen(true)}>
        Open from state
      </Button>
      <span className="text-sm text-muted-foreground">
        {open ? "Open" : "Closed"}
      </span>
      <Sheet open={open} onOpenChange={setOpen}>
        <SheetContent>
          <SheetHeader>
            <SheetTitle>Edit profile</SheetTitle>
            <SheetDescription>
              Changes are saved to your account when you click save.
            </SheetDescription>
          </SheetHeader>
          <SheetBody>
            <div className="flex flex-col gap-4">
              <div className="flex flex-col gap-1.5">
                <label
                  htmlFor="sheet-controlled-name"
                  className="text-sm font-medium"
                >
                  Name
                </label>
                <input
                  id="sheet-controlled-name"
                  defaultValue="Olivia Martin"
                  className="h-9 rounded-md border border-input bg-transparent px-3 text-sm transition-shadow outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden pointer-coarse:text-touch"
                />
              </div>
              <div className="flex flex-col gap-1.5">
                <label
                  htmlFor="sheet-controlled-username"
                  className="text-sm font-medium"
                >
                  Username
                </label>
                <input
                  id="sheet-controlled-username"
                  defaultValue="@olivia"
                  className="h-9 rounded-md border border-input bg-transparent px-3 text-sm transition-shadow outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden pointer-coarse:text-touch"
                />
              </div>
            </div>
          </SheetBody>
          <SheetFooter>
            <SheetClose render={<Button variant="outline" />}>
              Cancel
            </SheetClose>
            <SheetClose render={<Button />}>Save changes</SheetClose>
          </SheetFooter>
        </SheetContent>
      </Sheet>
    </div>
  )
}

Triggers desacoplados

Comparte un sheet entre varios triggers con createSheetHandle. Cada trigger pasa un payload que el sheet renderiza mediante un hijo función.

"use client"

import { Button } from "@/components/ui/button"
import {
  createSheetHandle,
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

const orders = createSheetHandle<{ id: string; status: string }>()

export function SheetDetached() {
  return (
    <div className="flex flex-wrap justify-center gap-2">
      <SheetTrigger
        handle={orders}
        payload={{ id: "#1042", status: "Shipped" }}
        render={<Button variant="outline" size="sm" />}
      >
        Order #1042
      </SheetTrigger>
      <SheetTrigger
        handle={orders}
        payload={{ id: "#1043", status: "Processing" }}
        render={<Button variant="outline" size="sm" />}
      >
        Order #1043
      </SheetTrigger>
      <Sheet handle={orders}>
        {({ payload }) => (
          <SheetContent>
            <SheetHeader>
              <SheetTitle>Order {payload?.id}</SheetTitle>
              <SheetDescription>Status: {payload?.status}</SheetDescription>
            </SheetHeader>
          </SheetContent>
        )}
      </Sheet>
    </div>
  )
}

De derecha a izquierda

Con dir="rtl", el lado derecho se refleja al borde izquierdo, entra desde allí y se desliza para salir en el mismo sentido.

import { Button } from "@/components/ui/button"
import {
  Sheet,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

export function SheetRtl() {
  return (
    <div dir="rtl">
      <Sheet>
        <SheetTrigger render={<Button variant="outline" />}>افتح</SheetTrigger>
        <SheetContent dir="rtl">
          <SheetHeader>
            <SheetTitle>تعديل الملف الشخصي</SheetTitle>
            <SheetDescription>
              يتم حفظ التغييرات عند النقر على حفظ.
            </SheetDescription>
          </SheetHeader>
          <SheetFooter>
            <SheetClose render={<Button variant="outline" />}>إلغاء</SheetClose>
            <SheetClose render={<Button />}>حفظ</SheetClose>
          </SheetFooter>
        </SheetContent>
      </Sheet>
    </div>
  )
}
KeyAcción
EnterSpaceEn el trigger, abre el sheet y mueve el foco a su interior.
TabShift + TabSe mueve entre elementos enfocables. El foco permanece dentro del sheet.
EscCierra el sheet superior y devuelve el foco a su trigger.
  • El sheet es un dialog modal. <SheetTitle /> lo etiqueta y <SheetDescription /> lo describe, así que incluye siempre un título.
  • El scroll de la página se bloquea y el contenido de detrás se oculta a las tecnologías de asistencia mientras está abierto.
  • El botón de cierre tiene la etiqueta “Close”. El tirador del borde interior indica que el sheet se puede arrastrar y se oculta a las tecnologías de asistencia.
  • Con movimiento reducido activado, el sheet aparece y desaparece con un fundido en lugar de deslizarse.

Construido sobre el drawer de Base UI. Cada parte acepta las props de la primitiva que envuelve.

PropTipoPredeterminado
defaultOpen
booleanfalse
open
boolean–
onOpenChange
(open: boolean, details) => void–
onOpenChangeCompleteSe llama cuando termina la animación de apertura o cierre.
(open: boolean) => void–
modal
boolean | "trap-focus"true
disablePointerDismissalLo mantiene abierto al hacer clic en el fondo.
booleanfalse
swipeDirectionSe define automáticamente según el lado y la dirección del contenido.
"up" | "down" | "left" | "right"–
handle
SheetHandle<Payload>–
children
ReactNode | ({ payload }) => ReactNode–
PropTipoPredeterminado
handle
SheetHandle<Payload>–
payload
Payload–
render
ReactElement | (props, state) => ReactElement<button>
AtributoDescripción
data-slot="sheet-trigger"Apunta al trigger en CSS.
data-popup-openPresente mientras su sheet está abierto.

Renderiza el portal, el fondo, el viewport y el popup, además del tirador de arrastre y el botón de cierre.

PropTipoPredeterminado
side
"top" | "right" | "bottom" | "left""right"
showCloseButton
booleantrue
dirDefine la dirección del sheet. right y left se reflejan en rtl.
"ltr" | "rtl"–
initialFocus
boolean | RefObject | (type) => HTMLElement | boolean–
finalFocus
boolean | RefObject | (type) => HTMLElement | boolean–
classNameLos sheets laterales ocupan el 75% del ancho, hasta 24rem desde el breakpoint sm.
string | (state) => string–
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescripción
data-slot="sheet-content"El panel del sheet.
data-sideEl lado desde el que se abre.
data-openPresente mientras el sheet está abierto.
data-starting-stylePresente mientras se anima la entrada.
data-ending-stylePresente mientras se anima la salida.
data-swipingPresente mientras se arrastra.
data-swipe-directionLa dirección que lo descarta.
data-nested-drawer-openPresente mientras hay un sheet anidado abierto encima.
data-slot="sheet-handle"El tirador de arrastre del borde interior.
data-slot="sheet-close-button"El botón de cierre integrado.
--drawer-swipe-movement-xDistancia de arrastre horizontal. También existe una variable -y.
--nested-drawersCuántos sheets anidados hay abiertos encima.

Lo renderiza <SheetContent />. Las capas anidadas reciben un fondo más claro.

AtributoDescripción
data-slot="sheet-overlay"El fondo.
data-nestedPresente en los fondos de las capas anidadas.

Elementos <div> simples que disponen el sheet. El encabezado deja espacio para el botón de cierre, el cuerpo hace scroll y ocupa el alto restante, y el pie apila sus acciones en pantallas pequeñas y las alinea al final desde el breakpoint sm.

AtributoDescripción
data-slot="sheet-header"Título y descripción.
data-slot="sheet-body"Contenido desplazable.
data-slot="sheet-footer"Acciones.
PropTipoPredeterminado
render
ReactElement | (props, state) => ReactElement<h2>
AtributoDescripción
data-slot="sheet-title"Etiqueta el sheet.
PropTipoPredeterminado
render
ReactElement | (props, state) => ReactElement<p>
AtributoDescripción
data-slot="sheet-description"Describe el sheet.
PropTipoPredeterminado
render
ReactElement | (props, state) => ReactElement<button>
AtributoDescripción
data-slot="sheet-close"Cierra el sheet al pulsarlo.

createSheetHandle<Payload>() devuelve un handle que conecta un <Sheet /> con triggers renderizados en otro lugar. Créalo una sola vez, fuera de tu componente.

Usado en bloques

Bloques que se construyen sobre Sheet.