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.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
actionsRef | React.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. |
defaultOpen | boolean | — | Heredada de Base UI. Si arranca abierto. Es la versión no controlada de open. |
modal | boolean | "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. |
open | boolean | — | 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.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
className | string | — | 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.
| Prop | Tipo | Por defecto | Descripció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. |
showCloseButton | boolean | true | El botón X de la esquina. Apagarlo deja al drawer sin control visible de cierre: si lo hacés, poné otro. |
showHandle | boolean | true | La barra de arrastre. Es decoración: apagala solo si el drawer no se puede arrastrar. |
className | string | — | Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana. |
finalFocus | boolean | 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ó. |
initialFocus | boolean | 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.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
className | string | — | Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana. |
DrawerFooter
Hereda las props de <div>.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
className | string | — | Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana. |
DrawerHandle
Hereda las props de <div>.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
className | string | — | Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana. |
DrawerHeader
Hereda las props de <div>.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
className | string | — | Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana. |
DrawerSwipeArea
Hereda las props de Drawer.SwipeArea.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
className | string | — | Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana. |
DrawerTitle
Hereda las props de Drawer.Title.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
className | string | — | 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
showCloseButtonviene 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. DrawerTitleno es opcional: es el nombre accesible del diálogo. Sin él, aviso por consola en desarrollo.- El movimiento pasa por
motion-reduce: conprefers-reduced-motionla 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— necesitadata-base-ui-swipe-ignorepara 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.
Sheetes unDialogpegado a un borde, sin gesto;DrawertraeswipeDirection,snapPoints, el área de swipe y el handle. Son dos patrones, ySheetya 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.Draweres 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. snapPointssolo tiene sentido conswipeDirectionvertical (downoup), 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 (conviewport-fit=cover, como enSheet). El gesto, los snap points y el handle no cambian. - El lado sale de
swipeDirectiondel root, no de una prop del contenido: una sola fuente de verdad para de dónde entra y hacia dónde se descarta. DrawerSwipeAreaabre con un swipe desde el borde, pero nunca va sola: sin unDrawerTriggeral lado, el drawer no existe para quien usa teclado.