Ir al contenido
Superposiciones

Drawer

La hoja que se arrastra: entra desde un borde, se cierra deslizándola y para en puntos de anclaje.

import { Drawer, DrawerBody, DrawerClose, DrawerContent, … } from "sebs7n-ui/drawer"

Ejemplos

Filtros en mobile

La hoja de abajo es el patrón de filtros del celular: entra desde el borde, se cierra con el pulgar hacia abajo y deja ver el listado de atrás. El `DrawerFooter` fija el "Aplicar" arriba del borde, donde llega la mano.

import { Button } from "sebs7n-ui/button"
import { DatePicker } from "sebs7n-ui/date-picker"
import { Drawer, DrawerBody, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "sebs7n-ui/drawer"
import { Input } from "sebs7n-ui/input"
import { Label } from "sebs7n-ui/label"

function FiltrosEnMobile() {
  return (
    <Drawer>
      <DrawerTrigger render={<Button variant="secondary" />}>Filtros</DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Filtrar facturas</DrawerTitle>
          <DrawerDescription>Se aplican al listado sin recargar la página.</DrawerDescription>
        </DrawerHeader>
        <DrawerBody className="flex flex-col gap-4 pb-5">
          <div className="flex flex-col gap-2">
            <Label htmlFor="drawer-cliente">Cliente</Label>
            <Input id="drawer-cliente" placeholder="Acme S.A." />
          </div>
          <div className="flex flex-col gap-2">
            <Label htmlFor="drawer-desde">Desde</Label>
            <DatePicker id="drawer-desde" />
          </div>
        </DrawerBody>
        <DrawerFooter>
          <DrawerClose render={<Button />}>Aplicar</DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  )
}

El detalle de un ítem, a media hoja

Con `snapPoints` la hoja para a mitad de camino: se ve el detalle sin tapar la lista, y se puede subir hasta el final. `defaultSnapPoint` elige dónde abre. El punto más alto pone `data-expanded` en el popup, por si el header tiene que cambiar ahí.

import { Badge } from "sebs7n-ui/badge"
import { Button } from "sebs7n-ui/button"
import { Drawer, DrawerBody, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "sebs7n-ui/drawer"

function DetalleAMediaHoja() {
  return (
    <Drawer defaultSnapPoint={0.45} snapPoints={[0.45, 1]}>
      <DrawerTrigger render={<Button variant="secondary" />}>Ver la factura 0012</DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Factura 0012</DrawerTitle>
          <DrawerDescription>Acme S.A., emitida el 14 de marzo.</DrawerDescription>
        </DrawerHeader>
        <DrawerBody className="flex flex-col gap-4 pb-5">
          <div className="flex items-center gap-2">
            <Badge color="green">Pagada</Badge>
            <Badge color="gray">3 ítems</Badge>
          </div>
          <dl className="flex flex-col gap-3 text-callout">
            <div className="flex justify-between">
              <dt className="text-label-secondary">Vencimiento</dt>
              <dd className="tabular-nums">13/04</dd>
            </div>
            <div className="flex justify-between">
              <dt className="text-label-secondary">Importe</dt>
              <dd className="tabular-nums">$ 128.400</dd>
            </div>
            <div className="flex justify-between">
              <dt className="text-label-secondary">Condición</dt>
              <dd>A 30 días</dd>
            </div>
          </dl>
        </DrawerBody>
        <DrawerFooter>
          <Button>Descargar el PDF</Button>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  )
}

Elegir de una lista larga

Lo que scrollea va adentro de `DrawerBody`: esa es la zona donde el dedo mueve la lista en vez de arrastrar la hoja. Si la lista estuviera suelta en el popup, cada intento de scrollear cerraría el drawer.

import { Button } from "sebs7n-ui/button"
import { Drawer, DrawerBody, DrawerClose, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, DrawerTrigger } from "sebs7n-ui/drawer"
import { useState } from "react"

function ElegirDeUnaListaLarga() {
  const [chofer, setChofer] = useState<string | null>(null)

  return (
    <div className="flex flex-col items-start gap-3">
      <Drawer>
        <DrawerTrigger render={<Button variant="secondary" />}>{chofer ?? "Asignar chofer"}</DrawerTrigger>
        <DrawerContent className="data-[swipe-direction=down]:max-h-[70%]">
          <DrawerHeader>
            <DrawerTitle>Asignar chofer</DrawerTitle>
            <DrawerDescription>Doce disponibles para el 14 de marzo.</DrawerDescription>
          </DrawerHeader>
          <DrawerBody className="pb-6">
            <ul className="flex flex-col">
              {CHOFERES.map((nombre) => (
                <li key={nombre}>
                  <DrawerClose
                    className="flex h-11 w-full items-center rounded-control px-2 text-left text-callout outline-none hover:bg-fill-2 focus-visible:focus-ring"
                    onClick={() => setChofer(nombre)}
                  >
                    {nombre}
                  </DrawerClose>
                </li>
              ))}
            </ul>
          </DrawerBody>
        </DrawerContent>
      </Drawer>
      {chofer && <p className="text-footnote text-label-secondary">Asignado: {chofer}</p>}
    </div>
  )
}

Props

Generadas del TypeScript del paquete. Las propias del componente, más las heredadas del primitivo que tienen algo que explicar —marcadas «heredada de Base UI»—. El resto está en la línea «hereda de».

Drawer

Hereda las props de Drawer.Root.

PropTipoPor defectoDescripción
actionsRefReact.RefObject<DrawerRoot.Actions | null>—Heredada de Base UI. Ref con las acciones imperativas de Base UI (unmount()), para desmontarlo sin esperar la animación de salida.
defaultOpenboolean—Heredada de Base UI. Si arranca abierto. Es la versión no controlada de open.
modalboolean | "trap-focus"—Heredada de Base UI. Con true, mientras está abierto el resto de la página no recibe clicks ni foco. Con "trap-focus" atrapa el foco pero deja pasar los clicks de afuera.
onOpenChange((open: boolean, eventDetails: DrawerRoot.ChangeEventDetails) => void)—Heredada de Base UI. Se llama con el estado nuevo cada vez que se abre o se cierra.
openboolean—Heredada de Base UI. Si está abierto. Pasarla lo vuelve controlado: sin onOpenChange ya no se cierra solo.

DrawerBody

Hereda las props de Drawer.Content.

PropTipoPor defectoDescripción
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.

DrawerClose

Hereda las props de Drawer.Close.

Sin props propias: pasa todo al primitivo.

DrawerContent

Hereda las props de Drawer.Popup.

PropTipoPor defectoDescripción
labels{ close?: string }—El texto del botón X. Con un LabelsProvider se traduce para toda la app; esta prop es la excepción de una pantalla puntual.
showCloseButtonbooleantrueEl botón X de la esquina. Apagarlo deja al drawer sin control visible de cierre: si lo hacés, poné otro.
showHandlebooleantrueLa barra de arrastre. Es decoración: apagala solo si el drawer no se puede arrastrar.
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.
finalFocusboolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | HTMLElement | void)—Heredada de Base UI. Qué recibe el foco al cerrar. Por defecto, lo que lo abrió.
initialFocusboolean | React.RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | HTMLElement | void)—Heredada de Base UI. Qué recibe el foco al abrir. Por defecto, el primer elemento tabulable de adentro.

DrawerDescription

Hereda las props de Drawer.Description.

PropTipoPor defectoDescripción
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.

DrawerFooter

Hereda las props de <div>.

PropTipoPor defectoDescripción
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.

DrawerHandle

Hereda las props de <div>.

PropTipoPor defectoDescripción
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.

DrawerHeader

Hereda las props de <div>.

PropTipoPor defectoDescripción
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.

DrawerSwipeArea

Hereda las props de Drawer.SwipeArea.

PropTipoPor defectoDescripción
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.

DrawerTitle

Hereda las props de Drawer.Title.

PropTipoPor defectoDescripción
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.

DrawerTrigger

Hereda las props de Drawer.Trigger.

Sin props propias: pasa todo al primitivo.

Teclado

Enter · Espacio
Abre desde el trigger.
Escape
Cierra y devuelve el foco al trigger. Es el camino de teclado, y no se puede apagar.
Tab · ⇧Tab
Recorre solo el contenido del drawer: el foco queda atrapado adentro.
Click en el fondo
Cierra.
—
No hay tecla para arrastrar. El gesto es un atajo: el cierre accesible es Escape y el botón X.

Accesibilidad

  • Un drawer que solo se cierra deslizando es un drawer que no se puede cerrar. Por eso showCloseButton viene prendido y Escape siempre funciona: arrastrar no es una opción con teclado, con switch control ni con una sola mano ocupada.
  • El handle es aria-hidden: es una pista visual, no un control. No recibe foco ni se anuncia, así que nunca cuenta como la salida del drawer.
  • DrawerTitle no es opcional: es el nombre accesible del diálogo. Sin él, aviso por consola en desarrollo.
  • El movimiento pasa por motion-reduce: con prefers-reduced-motion la hoja aparece en lugar de deslizarse.
  • Lo que arrastra es todo el popup menos DrawerBody. Un control que se maneja con el dedo adentro del área de arrastre —un slider, un canvas— necesita data-base-ui-swipe-ignore para que el gesto no se lo robe.

Reglas de uso

  • El dedo lo mueve → `Drawer`. Solo se lee y se cierra → `Sheet`. Está centrado y es una decisión → `Dialog`. Esa es toda la regla.
  • No reemplaza a `Sheet`, y `Sheet` no está construido sobre esto. Sheet es un Dialog pegado a un borde, sin gesto; Drawer trae swipeDirection, snapPoints, el área de swipe y el handle. Son dos patrones, y Sheet ya vive en producción: darle gesto por abajo sería un cambio de comportamiento que no aparece en ningún diff.
  • En desktop, casi siempre querés Sheet: nadie arrastra una hoja con el mouse. Drawer es para la mano.
  • Lo que scrollea va adentro de DrawerBody. Es la zona donde el dedo mueve el contenido en vez de la hoja; sin eso, cada intento de scrollear cierra el drawer.
  • snapPoints solo tiene sentido con swipeDirection vertical (down o up), que es el default.
  • Va pegado al borde (2.0), como Sheet: el radio del panel solo en las esquinas de adentro y el área segura de padding del lado de la pantalla (con viewport-fit=cover, como en Sheet). El gesto, los snap points y el handle no cambian.
  • El lado sale de swipeDirection del root, no de una prop del contenido: una sola fuente de verdad para de dónde entra y hacia dónde se descarta.
  • DrawerSwipeArea abre con un swipe desde el borde, pero nunca va sola: sin un DrawerTrigger al lado, el drawer no existe para quien usa teclado.

Relacionados