Bottom Sheet
Presenta contenido o acciones secundarias en una superficie que se desliza desde el borde inferior de la pantalla, sin perder de vista el contexto de la página de fondo. Es un patrón exclusivo para dispositivos mobile.
- Elegir entre varias opciones sin salir de la pantalla actual: desplegar una lista de acciones o de valores posibles (ordenar, filtrar, elegir una forma de envío) disparada desde un botón o un ítem, priorizando la interacción táctil en mobile gracias al grabber y a los snap points.
- Editar o completar un formulario corto sin perder el contexto de origen: mostrar campos de edición rápida que no necesitan una pantalla completa, pero sí más espacio que un Popover o un Dropdown. Ej.: editar una variante de producto, aplicar un cupón de descuento.
- Encadenar un sub-paso sobre un flujo ya abierto: abrir un segundo Bottom Sheet desde el contenido de uno ya visible, sin cerrar el primero (soportado de forma nativa: cada sheet se apila por orden de montaje y Escape solo cierra el que está más arriba). Ej.: elegir un medio de pago y, desde ahí, abrir la ayuda para configurarlo.
- Mostrar un panel persistente que convive con el resto de la pantalla sin bloquearla: el Bottom Sheet siempre renderiza una superposición oscura de fondo y bloquea la interacción con el resto de la página mientras está abierto; hoy no existe en Nimbus un modo "no bloqueante" para este patrón (por ejemplo, un reproductor persistente mientras se sigue navegando el resto de la app).
- Necesitar un panel lateral persistente en pantallas anchas: en desktop o tablet, un panel que ocupa el costado suele aprovechar mejor el espacio disponible que uno que se superpone desde abajo. En su lugar, usar Side Modal.
- Mostrar contenido tabular extenso con muchas columnas comparables: un sheet angosto no es un buen contenedor para tablas con scroll horizontal. En su lugar, usar Data Table o Data List.
- Mostrar un menú de pocas opciones anclado a un elemento puntual, sin necesidad de gestos de arrastre: para esos casos alcanza con algo más liviano. En su lugar, usar Popover.
Con Header y Footer
Solo Body
- Overlay: superficie oscura semitransparente que cubre el fondo mientras el sheet está abierto y bloquea la interacción con la página; tocarla cierra el sheet, salvo que se desactive con closeOnOutsidePress.
- Grabber: píldora arrastrable en el borde superior del panel; permite redimensionar el sheet entre los snapPoints disponibles o descartarlo con un arrastre hacia abajo, y también se opera desde el teclado.
- Header (opcional): encabezado del panel; suele mostrar el título de la acción o selección en curso. Si se omite, el sheet queda sin nombre accesible salvo que se pase aria-label o aria-labelledby a mano.
- Body: contenido scrolleable del sheet; es la única parte obligatoria de la composición.
- Footer (opcional): fila de acciones alineada a la derecha, reservada para confirmar o cancelar la selección en curso.
Default (["60%", "90%", "full"]): deja ver buena parte del contexto de fondo al abrir, con margen para expandir el sheet si el contenido no entra en el primer snap. Ej.: una lista mediana de opciones de envío.
Snap único y bajo (ej. ["40%"]): para contenido corto y predecible que no necesita expandirse, como una selección de 2 o 3 opciones. Ej.: "Ordenar por".
defaultSnap apuntando a "full": para contenido más largo que un formulario corto, como la edición completa de una variante con varios campos. El panel arranca a pantalla completa (esquinas superiores cuadradas) en vez de en un snap intermedio.
El Bottom Sheet es mobile-first: no tiene breakpoints de desktop propios ni cambia de layout entre dispositivos. Suele dispararse desde un botón, un ítem de lista o un ícono de acciones, y convive con el resto de los patrones flotantes de Nimbus (Popover, Modal, Sidebar) sin cerrarse si alguno de ellos se abre desde su propio contenido.
Usar snap points que dejen ver contexto de la pantalla de fondo cuando el contenido no lo necesita todo.
Evitar abrir siempre en full cuando el contenido entra en un snap más bajo: se pierde la ventaja de no bloquear del todo el contexto.
Incluir un Header con título cuando no se pasa aria-label: mantiene el sheet accesible y orienta a la persona sobre qué está viendo.
Evitar omitir el Header sin pasar aria-label: el sheet queda sin nombre accesible para lectores de pantalla.
Alinear las acciones de confirmación en el Footer, a la derecha.
Evitar poner botones de acción sueltos dentro del Body: rompe el patrón visual esperado del Footer.
- Rol y modalidad automáticos: el sheet aplica role="dialog" y aria-modal="true" por sí mismo; no hace falta agregarlos a mano.
- Nombre accesible condicionado al Header: si se usa BottomSheet.Header, el sheet queda vinculado a él vía aria-labelledby de forma automática. Si se omite, hay que pasar aria-label o aria-labelledby de forma explícita para que siga siendo accesible.
- Grabber operable sin gestos táctiles: implementado como un "movable splitter" (role="separator"), navegable con ArrowUp/ArrowDown/Home/End y con un área táctil de 56×56px sobre la píldora visual de 44×4px; moverlo con el teclado nunca cierra el sheet.
- Foco atrapado y restaurado: al abrirse, el foco queda contenido dentro del sheet, priorizando el primer elemento interactivo del contenido por sobre el Grabber; al cerrarse, se restaura en el elemento que lo disparó.
- Cierre consistente con la pila: si hay varios sheets abiertos, Escape solo cierra el que está más arriba; el click afuera se puede desactivar con closeOnOutsidePress cuando el flujo lo requiere.
- Traducir el grabberLabel: el valor por defecto viene en inglés ("Drag to resize or dismiss"); hay que pasar siempre un grabberLabel en español para que el lector de pantalla anuncie la instrucción en el idioma del sitio.
Instalá el componente via terminal.
npm install @nimbus-ds/bottom-sheetLista de opciones con Header, sin Footer. Cada opción cierra el sheet al seleccionarse.
import React, { useState } from "react";
import { BottomSheet } from "@nimbus-ds/patterns";
import { Box, Button, List, Text } from "@nimbus-ds/components";
import { mockSortOptions as sortOptions } from "lib/mocks/mock-labels";
const Example: React.FC = () => {
const [open, setOpen] = useState(false);
const [selected, setSelected] = useState(sortOptions[0]);
return (
<>
<Button onClick={() => setOpen(true)}>Ordenar por</Button>
<BottomSheet
open={open}
onRemove={() => setOpen(false)}
grabberLabel="Arrastrar para redimensionar o cerrar"
>
<BottomSheet.Header>
<Text fontWeight="bold" fontSize="highlight">
Ordenar por
</Text>
</BottomSheet.Header>
<BottomSheet.Body>
<List>
{sortOptions.map((option) => (
<List.Item key={option}>
<Box
as="button"
type="button"
width="100%"
display="flex"
justifyContent="space-between"
padding="2"
aria-pressed={selected === option}
onClick={() => {
setSelected(option);
setOpen(false);
}}
>
<Text fontWeight={selected === option ? "bold" : "regular"}>
{option}
</Text>
</Box>
</List.Item>
))}
</List>
</BottomSheet.Body>
</BottomSheet>
</>
);
};
export default Example;Las propiedades adicionales se pasan al elemento <BottomSheet>. Consultá la documentación del elemento div para ver la lista de atributos aceptados.
- Side Modal — alternativa de panel lateral persistente para pantallas anchas.
- Data Table — alternativa para contenido tabular extenso con muchas columnas.
- Data List — alternativa para listas de datos con muchos ítems y buscador.
- Popover — alternativa liviana para menús de pocas opciones sin gestos de arrastre.
BottomSheet
| Name | Type | Default | Description |
|---|---|---|---|
open | boolean | 'false' | Determines if the bottom sheet is shown or not. |
onRemove | object | Callback fired when the component requests to be closed (overlay press, close control, Escape, or a downward dismiss gesture). () => void; | |
snapPoints | array | '60%,90%,full' | Ordered list of heights the sheet can snap to. Each entry is a viewport-height percentage (e.g. "60%") or the keyword "full". |
defaultSnap | number | '0' | Index within `snapPoints` used as the initial snap point. |
children | React.ReactNode | Content of the sheet. Compose with BottomSheet.Header, BottomSheet.Body and BottomSheet.Footer. | |
closeOnOutsidePress | boolean | 'true' | Controls whether pressing outside should close the sheet. - boolean: enable/disable dismissal on outside press - function: receive the DOM event and return true to allow closing This is a responsive property and you can have the options below available for you to use. '{}' |
ignoreAttributeName | string | 'data-nimbus-outside-press-ignore' | The attribute name to ignore when checking for outside presses, so portaled Popover/Modal content does not close the sheet. |
needRemoveScroll | boolean | 'true' | Determines if background scroll is locked while the sheet is open. |
grabberLabel | string | 'Drag to resize or dismiss' | Accessible name (`aria-label`) for the drag grabber. Override for non-English locales, since the built-in default is English-only. |
zIndex | number | Explicit z-index for the sheet layer. Omitted by default: like Nimbus's own Sidebar/Modal/Popover, the sheet relies on DOM mount order rather than a z-index. Its portal mounts into the nearest Nimbus <ThemeProvider>'s own wrapper element (the same container Sidebar/Modal/Popover portal into), falling back to document.body only when no ThemeProvider is present. In both cases, later-mounted siblings paint on top of earlier ones as long as neither sets a z-index, so this keeps the sheet correctly stacked against Popover and other BottomSheet instances. Only set this to force a specific stacking order against something outside that convention. |
BottomSheet.Header
| Name | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | The content of the sheet header. Accepts any node (title, actions, icons). | |
padding | 'base' | 'base' | The padding around the header content area. |
BottomSheet.Body
| Name | Type | Default | Description |
|---|---|---|---|
children* | React.ReactNode | The scrollable content of the sheet. | |
padding | 'base' | 'base' | The padding around the body content area. |
BottomSheet.Footer
| Name | Type | Default | Description |
|---|---|---|---|
children* | React.ReactNode | The content of the sheet footer. | |
padding | 'base' | 'base' | The padding around the footer content area. |
Ayudanos a mejorar la documentación
¿Encontraste un problema o tenés una sugerencia? Contanos.