Saltar al contenido
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
Primeros pasosWebReactSwiftUIJetpack ComposeFlutterReact NativePreajustesTu propio sistema de diseñoUsar Tanrei con agentes de IAMarcaTokens de diseñoSistemas de diseño y personalizaciónTheme StudioColorTipografíaIconosFormaElevaciónMovimientoEstados de interacciónComportamientoDiseño de páginaAccesibilidadTextosTokens de colorTokens de la escala tipográficaTokens de formaTokens de elevaciónTokens de movimientoTokens de estadoTokens de espacio y de medidaComponentesBotónBotón de acción principalBotón segmentadoGrupo de botonesBotón con menúBarra de herramientasBotón de copiarCasilla de verificaciónBotón de opciónInterruptorChipMenúControl deslizanteCampo de control deslizanteCalendarioDateTimePickerGrupo de camposGrupo de botones de opciónGrupo de casillas de verificaciónSelectCampo de selecciónBarra de menúsSubida de archivosSelector de archivos AvanzadoZona de soltar archivosEntrada de texto AvanzadoCampo de textoCampo de chipsCampoCuadro combinado AvanzadoCampo de cuadro combinadoEntrada numérica AvanzadoCampo numéricoEntrada de color AvanzadoCampo de colorEntrada de código de un solo uso AvanzadoCampo de código de un solo usoEntrada de texto incrustada AvanzadoCuadro de redacción de chatTarjetaDivisorCapa modal AvanzadoDiálogoListaFila de listaHoja inferiorHoja lateralCarruselBloque de códigoTablaPopoverPasosAcordeónLista de descripcionesFiguraEstructura de la aplicaciónCabecera de páginaVista de árbolLínea de tiempoVista previa de imágenesTirador de tamañoMarco redimensionableÁrea de desplazamientoInsigniaIndicador de progresoMedidorContador animadoIndicador de cargaSnackbarTooltipAtajo de tecladoTagCódigo en líneaCalloutAvatarEstado vacíoGrupo de tagsEsqueletoElemento de archivoMuestraTexto en marquesinaPestañasVista de pestañasBarra de la aplicaciónBarra de navegaciónPanel de navegaciónBúsquedaEnlacePaginaciónEnlaces anterior y siguienteIndicador de pasosRuta de navegaciónTabla de contenidoTarjeta de enlaceMenú FABEstadoCómo se construye TanreiCómo contribuirLicencia y atribución
InicioComponentesHoja inferior

Hoja inferior

Hace subir desde el borde inferior un contenido complementario o una tarea breve, ya sea sobre la parte baja de la pantalla mientras el resto sigue siendo utilizable, o como un paso modal que atenúa y bloquea todo lo demás hasta que se descarta.

content_copyVer en Markdown
Categoría
Contención
Tokens
comp.bottom-sheet
Nivel
Compuesto
Se compone de
Modal layer

Descripción general

Web
tanrei-bottom-sheet
schedule
SwiftUI
tanreiBottomSheet
schedule
Jetpack Compose
TanreiBottomSheet
schedule
Flutter
TanreiBottomSheet
schedule
React Native
BottomSheet
schedule

Generado a partir de docs/status/status.json, specs/bottom-sheet.spec.json

Una hoja modal: ábrela con el botón

Guardar foto

Elige dónde guardar la foto. Puedes cambiarlo más adelante en Configuración.

Listo

Uso

Una hoja inferior es un componente controlado y nunca se cierra por sí misma. La abres con open. Cuando la persona pide que se vaya, la hoja emite close con el motivo, y tú vuelves a poner open en false. SwiftUI es la única excepción: ahí open es un binding, así que la hoja escribe false en él y después llama a onClose. Si dejas open en true, la hoja se queda, y tras un arrastre vuelve a asentarse en su altura más baja. En la web esto difiere del diálogo: la hoja cancela el evento nativo cancel y emite close en su lugar, de modo que Escape no la cierra hasta que lo haces tú.

close lleva uno de cinco motivos, con las mayúsculas y minúsculas propias de la plataforma:

Motivo Qué hizo la persona En qué forma
close Pulsó el botón de cierre de la hoja (closable) Estándar y modal
drag Soltó la hoja por debajo de la mitad de su altura más baja, o la lanzó hacia abajo desde ahí Estándar y modal
scrim Pulsó la zona atenuada que rodea una hoja modal Modal
escape Pulsó Escape Modal
back Usó la acción de retroceso de la plataforma: el botón o el gesto de retroceso de Android, el gesto de escape de accesibilidad de iOS. En la web, una petición de cierre que llega sin una tecla, como el gesto de retroceso de Android en el navegador, es back. Modal

La hoja emite close solo mientras dismissible es true, y exactamente una vez por cada petición. Mientras es false, el velo, Escape y la acción de retroceso no hacen nada, y un arrastre hacia abajo se asienta en la altura más baja. El botón de cierre es la excepción: pide cerrar con el motivo close diga lo que diga dismissible, porque es una salida que decidiste dibujar.

La hoja tiene dos formas en variant. Modal, la predeterminada, es una superposición modal: atenúa toda la ventana, mantiene el foco dentro y lo devuelve cuando se cierra. Estándar es una región del diseño de página, no una superposición: cubre la parte inferior del padre en el que la colocas, no dibuja ningún velo y deja que la persona siga trabajando en el contenido que hay encima.

Lo que va dentro de la hoja es cosa tuya. La hoja separa su contenido del borde con su margen interior (pon inset en false para una lista cuyas filas llevan su propio margen interior), lo recorta, le entrega un color de contenido y lo desplaza. Dale un headline y dibuja una cabecera sobre el contenido: el titular, un encabezado que además da nombre a la hoja cuando label está vacío, y, con closable activado, un botón de cierre que recibe su nombre de closeLabel: un botón small, tonal en el sistema predeterminado, como todos los botones de cierre de Tanrei. Bajo la cabecera hay un divisor según indique dividerVisibility, y la cabecera no se mueve mientras el contenido se desplaza. closable está desactivado de forma predeterminada, porque el tirador y, en una hoja modal, el velo son las salidas de la hoja; actívalo en una hoja estándar o en una hoja sin tirador.

Marca el elemento que debe recibir el foco cuando se abre la hoja con la marca propia de la plataforma: autofocus en la web, .defaultFocus o un @AccessibilityFocusState en SwiftUI, un FocusRequester pasado como initialFocus en Compose, autofocus: true en Flutter e initialFocusRef en React Native. Sin una marca, el foco va a la hoja en sí, de modo que su etiqueta se lee primero.

<tanrei-button id="save" variant="outlined">Guardar foto</tanrei-button>

<!-- Modal es la forma predeterminada. closedby="none" equivale a dismissible en false. -->
<tanrei-bottom-sheet id="save-sheet" label="Guardar foto" handle-label="Cambiar el tamaño de la hoja">
  <p>Elige dónde guardar la foto.</p>
  <tanrei-button variant="text" autofocus>Listo</tanrei-button>
</tanrei-bottom-sheet>

<!-- Estándar: cubre la parte inferior de su bloque contenedor. flush equivale a inset en false; no-handle, a handle en false. -->
<div class="map" style="position: relative">
  <tanrei-bottom-sheet variant="standard" open label="Lugares cercanos" handle-label="Cambiar el tamaño de la hoja" flush>
    <tanrei-list>…</tanrei-list>
  </tanrei-bottom-sheet>
</div>

<script type="module">
  import '@tanrei/web/components/bottom-sheet';
  const sheet = document.querySelector('#save-sheet');
  document.querySelector('#save').addEventListener('click', () => (sheet.open = true));
  sheet.querySelector('tanrei-button').addEventListener('click', () => (sheet.open = false));
  // La hoja nunca se cierra por sí misma. detail.reason es drag, scrim, escape o back.
  sheet.addEventListener('close', (e) => (sheet.open = false));
  // La persona la movió a su otra altura. La hoja ya ha definido detent.
  sheet.addEventListener('detentchange', (e) => console.log(e.detail.detent));
</script>
import { BottomSheet, Button } from '@tanrei/react';

<BottomSheet
  open={open}
  label="Guardar foto"
  handleLabel="Cambiar el tamaño de la hoja"
  onClose={() => setOpen(false)}
  onDetentChange={(e) => setDetent(e.detail.detent)}
>
  <p>Elige dónde guardar la foto.</p>
  <Button variant="text" onClick={() => setOpen(false)}>Listo</Button>
</BottomSheet>
import Tanrei

// Un modificador en la vista que cubre la hoja, como .sheet. Al cerrar, escribe false en el binding
// y después llama a onClose con el motivo. La altura que elige la persona se escribe en detent.
PhotoView(photo)
    .tanreiBottomSheet(isPresented: $saving, detent: $detent,
                       handleLabel: "Cambiar el tamaño de la hoja", label: "Guardar foto") {
        SaveTargets(photo)
        TanreiButton("Listo", variant: .text) { saving = false }
    }

// Estándar: una superposición en la parte inferior de la vista que cubre.
MapView()
    .tanreiBottomSheet(isPresented: $placesShown, variant: .standard,
                       handleLabel: "Cambiar el tamaño de la hoja", label: "Lugares cercanos") {
        PlacesList()
    }
import io.github.chnotchy.tanrei.components.TanreiBottomSheet
import io.github.chnotchy.tanrei.components.TanreiButton
import io.github.chnotchy.tanrei.tokens.TanreiButtonVariant

// visible, en lugar de llamarla solo mientras está abierta, para que pueda ejecutarse el movimiento de salida.
// Ponlo en false desde onClose, que recibe el motivo.
TanreiBottomSheet(
    visible = saving,
    onClose = { saving = false },
    detent = detent,
    onDetentChange = { detent = it },
    handleLabel = "Cambiar el tamaño de la hoja",
    contentDescription = "Guardar foto",
) {
    SaveTargets(photo)
    TanreiButton(text = "Listo", onClick = { saving = false }, variant = TanreiButtonVariant.Text)
}
import 'package:tanrei/tanrei.dart';

// Una hoja modal es el último hijo de un Stack que cubre toda la página. Pon open en false desde onClose.
Stack(
  children: <Widget>[
    const Positioned.fill(child: PhotoPage()),
    TanreiBottomSheet(
      open: saving,
      onClose: (TanreiSheetCloseReason reason) => setState(() => saving = false),
      detent: detent,
      onDetentChanged: (value) => setState(() => detent = value),
      handleSemanticLabel: 'Cambiar el tamaño de la hoja',
      semanticLabel: 'Guardar foto',
      child: const SaveTargets(),
    ),
  ],
),
import { useRef, useState } from 'react';
import type { View } from 'react-native';
import { BottomSheet, Button } from '@tanrei/react-native';

const done = useRef<View>(null);
const [open, setOpen] = useState(false);

// Controlada: pon open en false desde onClose, que recibe el motivo ('drag', 'scrim', 'escape' o 'back').
// Una hoja modal se presenta en un Modal transparente. La propiedad insets de la hoja sustituye los lados que nombra de los insets de TanreiProvider.
<BottomSheet
  open={open}
  onClose={(reason) => setOpen(false)}
  detent={detent}
  onDetentChange={setDetent}
  handleLabel="Cambiar el tamaño de la hoja"
  accessibilityLabel="Guardar foto"
  initialFocusRef={done}
>
  <SaveTargets />
  <Button ref={done} variant="text" onPress={() => setOpen(false)}>Listo</Button>
</BottomSheet>

Especificaciones

Una hoja estándar sobre la parte inferior de su padre: arrástrala hacia abajo para cerrarla

Mapa

Mostrar lugares cercanos

Hay tres cafeterías y una panadería abiertas a menos de diez minutos a pie.

Anatomía

Parte Descripción
1 scrim · opcional The modal layer's scrim over everything behind a modal sheet (MDL-1). It blocks presses and, while the sheet is dismissible, a press on it asks the sheet to close. A standard sheet draws none.
2 container The raised surface docked to the bottom edge of the area it covers, centred and no wider than its maximum width, its top corners rounded and its bottom corners square. Its color reaches under the bottom system inset.
3 drag-handle · opcional The control at the top of the sheet: a Handle without a value, always drawn, that carries hover, focus and press on its box, with the grip centred in it. Activating it moves the sheet between its two heights.
4 drag-handle.grip · opcional The Handle's line: the short rounded bar drawn in the handle's box that says the sheet can be dragged.
5 header · opcional The row below the drag handle's row (or at the top without one) that holds the headline and the close button. Drawn while headline is not empty or closable is true; it stays in place while the content scrolls.
6 headline · opcional What the sheet is about ("Share photo"), a heading. It names the sheet where label is empty.
7 close-button · opcional An icon-only Button of the small size showing the close glyph at the end of the header, in the variant comp.bottom-sheet.close-button.variant names (tonal in the default system), as every host's close button is. The sheet places it and chooses its variant; everything else it reads from Button's own tokens.
8 divider · opcional A hairline across the sheet below the header: drawn whenever the header is in the default system, or only while the content is scrolled away from its top under dividerVisibility scroll.
9 content Whatever the product puts in the sheet: a list of choices, a form, details. The sheet insets it (unless inset is false), clips it, hands it a content color and scrolls it; it does not lay it out.
10 focus-ring · opcional Gap-then-ring indicator around the drag handle's box, shown for keyboard focus.

Propiedades

Propiedad Tipo Valor predeterminado Descripción
variant standard | modal "modal" Standard sits over the lower part of the content while the content above stays usable (a layout region); modal dims and blocks everything else and holds focus until it closes (a modal overlay). Both read the same container tokens: the scrim and the behaviour set them apart. Not a token axis.
open boolean false Whether the sheet is shown. The product opens it, and closes it in answer to close (or from its own controls); on SwiftUI, where it is a binding, the sheet writes false itself.
detent partial | full "partial" The height the sheet rests at while its content is taller than it: partial takes comp.bottom-sheet.container.partial-fraction of the area; full reaches up to comp.bottom-sheet.container.margin-block-start below the area's top. A sheet whose content is shorter than its partial height has one height, and detent changes nothing. The person moves between the two by dragging or with the drag handle (detentChange).
dismissible boolean true Whether a drag below the sheet's lowest height asks it to close and, for a modal sheet, whether Escape, the platform's back and a press on the scrim do too. Set it to false only when the person must finish the sheet's task with one of its own controls.
handle boolean true Draws the drag handle at the top of the sheet. Without it the sheet can still be dragged by its padding and content (BSH-9), but a keyboard or switch user has no way to change its height, so leave it on for a sheet whose content can be taller than its partial height.
handleLabel string "" The drag handle's accessible name ("Resize sheet"); its expanded state is exposed with it. Required while the handle is interactive (BSH-11).
inset boolean true Insets the content by comp.bottom-sheet.container.padding. Set it to false for content that pads itself or reaches the edges (a list whose rows carry their own padding).
label string "" Names the sheet for assistive technology ("Share photo"): the modal dialog, or the standard sheet's region. Required unless headline names it.
headline string "" What the sheet is about ("Share photo"), drawn in its header (BSH-28). While label is empty it is the sheet's accessible name.
closable boolean false Draws the close button at the end of the header (BSH-29). False by default: the drag handle and, for a modal sheet, the scrim are the sheet's ways out. Set it where a person who cannot drag needs one they can see — a standard sheet, or a sheet without a handle.
closeLabel string "" The close button's accessible name ("Close sharing"). Required while closable is true.
dividerVisibility auto | always | scroll "auto" When the divider below the header is drawn. always: whenever the header is drawn, at rest as well as while the content is scrolled. scroll: only while the content is scrolled away from its top. auto takes comp.bottom-sheet.divider.visibility, the design system's answer (always in the default system).

Slots

Slot Descripción
default The content. A fixture's content string is plain text, which takes sys.typescale.body in the content color (BSH-8).

Eventos

Evento Descripción
close The sheet asks to be closed: its close button (BSH-29), a released drag below its lowest height, or for a modal sheet a press on the scrim, Escape or the platform's back. Carries which of these it was, named close, drag, scrim, escape or back (in the platform's casing). The close button asks whatever dismissible says; the others only while dismissible is true.
detentChange The person moved the sheet to its other height, by a drag or with the drag handle. Carries the new detent (partial or full). Not emitted when the product sets detent.

Estados

Comportamiento

Requisitos en formato EARS. Cada implementación de plataforma se prueba contra estos identificadores. El texto de los requisitos se muestra tal como está en la especificación, en inglés.

ID Requisito
BSH-1 WHEN open becomes true THE bottom sheet SHALL slide its container up from below the bottom edge of the area it covers to its height (BSH-6) — WHILE variant is modal over the whole window on its modal layer, fading the layer's scrim in comp.bottom-sheet.scrim.color and comp.bottom-sheet.scrim.blur in behind it in step (MDL-1, MDL-7); WHILE variant is standard over the bottom of the parent the product placed it in, with no scrim — over comp.bottom-sheet.motion.enter-duration with comp.bottom-sheet.motion.enter-easing; WHEN open becomes false THE bottom sheet SHALL slide it down out of view from wherever it is and fade the scrim out, over comp.bottom-sheet.motion.exit-duration with comp.bottom-sheet.motion.exit-easing. WHERE motion is reduced it SHALL fade in and out in place without sliding, or appear and disappear at once on a platform whose reduced-motion setting removes every animation (Android's). A sheet that is mounted with open true SHALL appear in place without motion.
BSH-2 WHILE variant is modal and the sheet is open THE bottom sheet SHALL make everything outside it inert, keep the content behind it from scrolling and keep focus inside it, as its modal layer does (MDL-2, MDL-4): a modal overlay.
BSH-3 WHEN open changes from false to true THE bottom sheet SHALL move focus to the element in it that the product marked for initial focus (the platform's own mark, named in the platform notes) or, without one, to its container, so that assistive technology reads its label first; WHEN it closes while focus is inside it THE bottom sheet SHALL return focus to the element that had it when the sheet opened or, where that element no longer exists or cannot take focus, to the platform's default for a closed overlay. Focus SHALL move when the sheet opens or closes, not when its motion ends. This holds for both variants (a modal sheet moves and returns focus as its modal layer does, MDL-3, MDL-5); a standard sheet mounted with open true SHALL NOT move focus, and a modal sheet mounted with open true SHALL move focus in as if it had just opened, because it makes everything behind it unfocusable and focus must be somewhere it can reach.
BSH-4 WHILE variant is standard THE bottom sheet SHALL draw no scrim, SHALL NOT make anything inert, SHALL NOT keep focus inside it, SHALL let Tab and Shift+Tab move focus between it and the content it covers without closing it, and SHALL ignore Escape and the platform's back: a standard sheet is a layout region, not an overlay. The product places it over the content it covers, in a parent that holds both; the sheet SHALL dock to that parent's bottom edge, SHALL NOT resize or pad the content, and SHALL let presses on the part of the parent above it reach the content.
BSH-5 WHILE dismissible is true, WHEN the user presses the scrim, presses Escape or uses the platform's back WHILE variant is modal, as its modal layer answers them (MDL-6), or releases a drag that asks to close (BSH-9) in either variant, THE bottom sheet SHALL emit close exactly once with the reason scrim, escape, back or drag, and SHALL then be closed: through its binding where open is one (SwiftUI), and by the product setting open to false where open is a plain value (web, Compose, Flutter, React Native). WHILE dismissible is false THE bottom sheet SHALL ignore these inputs and emit nothing for them (its close button, BSH-29, still asks); a press on the scrim SHALL still reach nothing behind it.
BSH-6 THE bottom sheet SHALL be as tall as its drag handle's row, its header (BSH-28) and its content (with its insets), but no taller than its detent allows: WHILE detent is partial, comp.bottom-sheet.container.partial-fraction of the height of the area it covers; WHILE detent is full, that height less comp.bottom-sheet.container.margin-block-start. The area is the window below its top system inset for a modal sheet, and the parent the product placed it in for a standard sheet; WHILE the on-screen keyboard is shown the area SHALL end at the keyboard's top edge, so that the sheet sits on the keyboard and both heights are taken from the smaller area. The sheet has two heights only WHILE its content is taller than its partial height; otherwise it has one, and detent changes nothing.
BSH-7 THE bottom sheet SHALL draw its container at the bottom edge of the area, as wide as the area but no wider than comp.bottom-sheet.container.max-width and centred in it, filled with comp.bottom-sheet.container.color, raised by comp.bottom-sheet.container.elevation, with its top corners rounded by comp.bottom-sheet.container.shape and its bottom corners square; it SHALL extend its color under the bottom system inset and keep its content above that inset, and WHILE the on-screen keyboard is shown it SHALL sit on the keyboard (BSH-6) with its content above it.
BSH-8 THE bottom sheet SHALL lay out, top to bottom, the drag handle's row WHILE handle is true (BSH-12), the header WHILE there is one (BSH-28) and then its content. WHILE inset is true it SHALL inset the content by comp.bottom-sheet.container.padding from its start, end and bottom edges, and from its top edge WHILE handle is false and no header is drawn; WHILE inset is false the content SHALL reach the container's edges, below the handle's row and the header. It SHALL clip the content to the container's shape, SHALL hand it comp.bottom-sheet.content.color as the platform's inherited foreground (the web's inherited color, SwiftUI's foreground style, Compose's content color, Flutter's default text and icon style, React Native's Tanrei text context), and plain text given as content, where the platform must style it, SHALL take sys.typescale.body.
BSH-9 WHEN the user drags the sheet vertically — by its handle's row, by the padding around its content, or by its content where BSH-10 lets the drag reach the sheet — beyond the platform's touch slop THE bottom sheet SHALL follow the finger or pointer, never above its full height; WHEN the drag is released it SHALL settle at the next height in the direction of a fling, or else at the height nearest to where it was released, over comp.bottom-sheet.motion.enter-duration with comp.bottom-sheet.motion.enter-easing (at once where motion is reduced). A release below half of the sheet's lowest height, or a downward fling from its lowest height, SHALL ask to close WHILE dismissible is true (BSH-5, BSH-20); WHILE dismissible is false the sheet SHALL settle at its lowest height instead. A fling is a release faster than the platform's minimum fling velocity (Android's ViewConfiguration.getScaledMinimumFlingVelocity, Flutter's kMinFlingVelocity) or, where the platform names none (the web, SwiftUI, React Native), 0.5 points per millisecond; the touch slop is the platform's own (Android's ViewConfiguration.getScaledTouchSlop, Flutter's kTouchSlop) or, where the platform names none, 8 points. A release's velocity is the pointer's over its last moves: WHERE the pointer has not moved for longer than the time after which the platform's velocity tracker takes a pointer as stopped (40 milliseconds in Android's VelocityTracker, Jetpack Compose's VelocityTracker and Flutter's VelocityTracker) or, where the platform names none (the web, SwiftUI, React Native), 40 milliseconds before the release THE bottom sheet SHALL take the release as having no velocity, so that a drag that stops where it is wanted and is let go there is no fling, however fast it moved before it stopped. WHEN a drag ends at the height it did not start from THE bottom sheet SHALL set detent to it and emit detentChange exactly once with it.
BSH-10 WHILE the content is taller than the room the sheet gives it THE bottom sheet SHALL scroll the content inside the container, below the handle's row, and SHALL let assistive technology and keyboard focus scroll it at either height; a drag upward on the content SHALL first raise the sheet to its full height and then scroll the content, and a drag downward on the content SHALL first scroll the content to its top and then lower the sheet.
BSH-11 WHILE handle is true and the sheet has two heights THE bottom sheet SHALL make the drag handle one focus stop, a button named by handleLabel with the expanded state, true WHILE detent is full (its label and expanded, HDL-8), and WHEN the user activates it by pointer, touch, Enter or Space (its activate, HDL-7) THE bottom sheet SHALL move to its other height as BSH-9 settles and emit detentChange exactly once with the new detent; WHILE the sheet has one height the handle SHALL still be drawn at rest but SHALL NOT be focusable, pressable or exposed to assistive technology. WHILE the handle is interactive and handleLabel is empty it SHALL still render, and in debug builds report that the handle has no accessible name.
BSH-12 WHILE handle is true THE bottom sheet SHALL draw at its top a row holding the drag handle — a Handle of axis vertical and size medium, with no value and revealed — centred across the sheet with comp.bottom-sheet.handle.margin-block above and below it, and SHALL give that Handle its own tokens in place of the Handle's: the box comp.bottom-sheet.handle.container-width wide and comp.bottom-sheet.handle.container-height high with comp.bottom-sheet.handle.container-shape corners (HDL-3), the grip — the Handle's line — comp.bottom-sheet.handle.grip-width long and comp.bottom-sheet.handle.grip-height thick (HDL-1), and the container, line and state-layer colors of each state from the drag-handle control (comp.bottom-sheet.drag-handle.container.color, comp.bottom-sheet.drag-handle.grip.color, comp.bottom-sheet.drag-handle.state-layer.color, and so on for each state), its opacity in each state, and comp.bottom-sheet.motion.state-duration, comp.bottom-sheet.motion.state-easing and comp.bottom-sheet.motion.feedback-duration as its motion; in the default system each of them aliases the Handle's own. The row SHALL stay in place while the content scrolls.
BSH-13 WHILE the drag handle is hovered, keyboard-focused or pressed THE bottom sheet SHALL render the handle's box, grip and opacity from the component tokens of the highest-precedence active state, with precedence pressed over focused over hovered, and the grip at the Handle's length and thickness of a handle in use (HDL-4); focus gained by pointer or touch SHALL NOT select the focused state. A drag that starts on the handle SHALL keep it pressed while the sheet follows the drag, as a Handle is held for as long as a drag lasts (HDL-11).
BSH-14 WHEN the drag handle receives keyboard focus THE bottom sheet SHALL show the focus ring around the handle's box without animation; focus gained by pointer or touch SHALL NOT show it.
BSH-15 THE bottom sheet SHALL give the drag handle the Handle's hit area (HDL-5): at least sys.target.touch wide and high for touch input and sys.target.pointer for fine pointers, centred on its box, without changing its drawn size, and the Handle's vertical resize cursor; a platform that cannot tell the input type SHALL use sys.target.touch. A press on the rest of the handle's row drags the sheet as BSH-9 says.
BSH-16 WHEN the drag handle is pressed by pointer, touch, Enter or Space THE drag handle SHALL show the pressed state without delay and SHALL keep it visible for at least sys.motion.duration.press-hold, even when the press ends sooner (HDL-11).
BSH-17 WHILE pressed THE drag handle SHALL answer as sys.behavior.pressed-feedback names: overlay applies the pressed tokens to the whole container at once; ripple grows the pressed state layer from the point of contact (from the center for keyboard activation); dim applies the pressed opacity to the whole control and draws no state layer; none draws no answer to the press. Under ripple and dim only the container color stays with the state under the press; grip and opacity still come from the pressed tokens. Under none every part stays with the state under the press. The ripple grows over comp.bottom-sheet.motion.feedback-duration with comp.bottom-sheet.motion.state-easing, fades over comp.bottom-sheet.motion.state-duration with comp.bottom-sheet.motion.state-easing once the press has ended and press-hold has passed, and appears whole where motion is reduced.
BSH-18 THE bottom sheet SHALL ease its container's top corners and the drag handle's box corners by sys.shape.corner-smoothing (0 is a quarter circle, 0.6 the iOS continuous corner; a platform with only the OS curve draws it for any amount above 0) and show the handle's keyboard focus as sys.behavior.focus-style names (ring or glow); a platform that cannot draw a choice SHALL fall back to the first value and state that in its README.
BSH-19 WHEN the drag handle's colors, or its grip's length and thickness, change between states THE bottom sheet SHALL animate them over comp.bottom-sheet.motion.state-duration with comp.bottom-sheet.motion.state-easing, and SHALL change them at once where motion is reduced.
BSH-20 WHEN a released drag asks to close THE bottom sheet SHALL emit close (BSH-5), hold where the drag left it for one frame, and then slide out from there as BSH-1 says IF open has become false, or settle back at its lowest height as BSH-9 says IF open is still true (the product declined to close it).
BSH-21 WHEN the product changes detent while the sheet is open THE bottom sheet SHALL move to the new height over comp.bottom-sheet.motion.enter-duration with comp.bottom-sheet.motion.enter-easing (at once where motion is reduced) and SHALL NOT emit detentChange; where detent is a plain value (web, Compose, Flutter, React Native), a height the person chose (BSH-9, BSH-11) SHALL stay until the product sets detent again, and where it is a binding (SwiftUI) the sheet SHALL write the new detent to it.
BSH-22 THE bottom sheet SHALL expose its container, WHILE variant is modal, as a modal dialog named by label and, WHILE variant is standard, as a region named by label (a landmark on the web) — by the headline WHILE label is empty (BSH-28); WHILE label and headline are both empty it SHALL still render, and in debug builds report that it has no accessible name. The drag handle's grip, the Handle's line, SHALL be hidden from assistive technology; the handle itself is exposed only as BSH-11 says.
BSH-23 WHEN a dialog, a menu or another modal sheet opens from a modal bottom sheet THE bottom sheet SHALL stay below it, become inert with the rest of the window, and on the other overlay's closing receive back the focus that overlay returns, as its modal layer does (MDL-8, MDL-9); Escape SHALL dismiss only the topmost overlay.
BSH-24 WHILE the text size is enlarged THE bottom sheet SHALL grow with its content up to the height its detent allows and scroll the content beyond it (BSH-10), grow its header with a headline that wraps, and SHALL NOT scale the drag handle, nor the close button beyond what the Button itself does.
BSH-25 WHEN variant or detent names a value the bottom sheet does not define THE bottom sheet SHALL render with the default value, and in debug builds report the unknown value.
BSH-26 WHEN the layout direction is right-to-left THE bottom sheet SHALL keep its container and its drag handle centred, and its content SHALL follow the layout direction.
BSH-27 THE bottom sheet SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the bottom sheet is shown THE bottom sheet SHALL draw itself for the new direction at once, without a motion of its own, keeping its open state, its height and its focus.
BSH-28 WHILE headline is not empty or closable is true THE bottom sheet SHALL draw a header below the drag handle's row (or at the top of the container without one): a row at least comp.bottom-sheet.header.height high holding the headline from comp.bottom-sheet.container.padding after the start edge, in comp.bottom-sheet.headline.typescale and comp.bottom-sheet.headline.color, wrapping onto more lines rather than truncating and growing the row with it, exposed as a heading, and, WHILE closable is true, the close button ending comp.bottom-sheet.header.padding-inline-end before the end edge and comp.bottom-sheet.header.gap after the headline, both centred in the row; below it, a divider comp.bottom-sheet.divider.width thick in comp.bottom-sheet.divider.color across the sheet's full width WHILE dividerVisibility (or, for auto, comp.bottom-sheet.divider.visibility) is always, or is scroll and the content is scrolled away from its top, shown and hidden at once. The header SHALL NOT scroll with the content, and a drag on it SHALL move the sheet as a drag on the handle's row does (BSH-9). WHILE headline is not empty it SHALL name the sheet where label is empty. A dividerVisibility the sheet does not define SHALL be drawn as auto, and reported in debug builds.
BSH-29 WHILE closable is true THE bottom sheet SHALL draw its close button as an icon-only Button of the small size showing the glyph close of the system's icon set and named by closeLabel, in the variant comp.bottom-sheet.close-button.variant names, given through the channel a Button reads from the component that places it, and otherwise unrestyled: it reads Button's own tokens, answers hover, focus and press as any Button does and keeps its own minimum target (BTN-5); WHEN the user activates it THE bottom sheet SHALL emit close exactly once with the reason close, in either variant and whatever dismissible says, and SHALL then be closed as BSH-5 says. WHILE closable is true and closeLabel is empty it SHALL still draw the button, and in debug builds report that the button has no accessible name. THE bottom sheet SHALL draw no close mark of its own.
BSH-30 WHILE handle is false, dismissible is true and closable is false THE bottom sheet SHALL still render, and in debug builds report that it shows no way out a person can see: a drag, Escape and the scrim still close it, but nothing on screen says so to a person who cannot drag.
BSH-31 WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on the host, by the attribute or by the ARIA property that reflects it, THE bottom sheet SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on its container (BSH-22) — its dialog WHILE variant is modal, its region otherwise — an id reference by the elements it names in the host's tree or a tree around it, a description after its own. WHILE label or headline is not empty it is the container's name (BSH-22), over a name given so; WHILE both are empty, a name given so is the container's name and SHALL NOT be reported as having none in debug builds. WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE bottom sheet SHALL take it off that element.

Correspondencia de nombres por plataforma

Paridad no significa API idénticas. Cada plataforma expresa el mismo contrato con su propio estilo.

Especificación Web SwiftUI Jetpack Compose Flutter React Native
Componente tanrei-bottom-sheet tanreiBottomSheet TanreiBottomSheet TanreiBottomSheet BottomSheet
variant · Propiedad variant variant variant variant variant
open · Propiedad open isPresented visible open open
detent · Propiedad detent detent detent detent detent
dismissible · Propiedad closedby dismissible dismissible dismissible dismissible
handle · Propiedad handle handle handle showHandle handle
handleLabel · Propiedad handleLabel handleLabel handleLabel handleSemanticLabel handleLabel
inset · Propiedad inset inset inset inset inset
label · Propiedad label label contentDescription semanticLabel accessibilityLabel
headline · Propiedad headline headline headline headline headline
closable · Propiedad closable closable closable showCloseButton closable
closeLabel · Propiedad closeLabel closeLabel closeLabel closeButtonSemanticLabel closeLabel
dividerVisibility · Propiedad dividerVisibility dividerVisibility dividerVisibility dividerVisibility dividerVisibility
close · Evento close onClose onClose onClose onClose
detentChange · Evento detentchange detent onDetentChange onDetentChanged onDetentChange
Notas dismissible is HTML's own closedby attribute, as on <tanrei-dialog>: any (the default) or none. handle and inset are boolean properties that default to true; their attributes are the inverses no-handle and flush (an HTML boolean attribute cannot default to true). handleLabel is the handle-label attribute, closeLabel the close-label attribute and dividerVisibility the divider-visibility attribute (auto reads --tanrei-comp-bottom-sheet-divider-visibility from the cascade with a style query, and where the browser has none the sheet writes the name it read on its header when it renders). The close button is a <tanrei-button> in the shadow root, icon-only (the close glyph in its icon slot, its label from close-label), whose variant comes from the resolved comp.bottom-sheet.close-button.variant through the internal channel a Button reads from its host; the headline is an <h2>. close is a CustomEvent whose detail.reason is close, drag, scrim, escape or back (back when a modal sheet's native cancel arrives without an Escape key, as Android's back gesture sends it), and detentchange one whose detail.detent is the new detent; the element sets detent itself before firing it. The product sets open to false from close. The content is the default slot. Drags use pointer events with touch-action: none on the handle's row and the top edge; the content scrolls natively and hands the drag over at its top (BSH-10). Fling 0.5 px/ms, slop 8 px. Insets are env(safe-area-inset-bottom); the on-screen keyboard is read from visualViewport. Snapshot harness: a 400 × 720 viewport with a surface page; the sheet opened by open; the screenshot is the viewport, since a modal sheet and its backdrop are in the top layer. The drag handle is a <tanrei-handle> in the shadow root, its container and line exported as the parts handle and grip; the sheet sets the Handle's custom properties to its own tokens on it (BSH-12), answers its activate, and keeps it inert while the sheet has one height (BSH-11). A view modifier on the view the sheet covers, as .sheet is: .tanreiBottomSheet(isPresented: Binding<Bool>, variant:, detent: Binding<TanreiBottomSheetDetent>, dismissible:, handle:, handleLabel:, inset:, label:, onClose: ((TanreiSheetCloseReason) -> Void)?, content:). Closing writes false to isPresented and then calls onClose with the reason; a height the person chose is written to detent (detentChange). Modal: on iOS presented with .fullScreenCover, a clear presentation background and the system transition off, as tanreiDialog is (that presentation hides the rest of the app from VoiceOver); Tanrei draws the scrim, the sheet and the drag with a DragGesture (fling from its velocity, 0.5 pt/ms). Not the native .sheet: its dim cannot take the scrim token, and on iPad in a regular width it becomes a centred form, not a bottom sheet. macOS, which has no full-screen cover, draws the modal sheet as an overlay of the modified view with the modal trait. Standard: .overlay(alignment: .bottom) of the modified view. Initial focus is .defaultFocus or an @AccessibilityFocusState the product passes; Escape is .onKeyPress(.escape) and the escape gesture .accessibilityAction(.escape). Snapshot harness: renders TanreiBottomSheetSurface (the sheet and, for a modal sheet, the scrim) in place in a 400 × 720 frame over a surface page, because a snapshot of the presenting view does not include a presented window. Not built on this platform yet: the header — headline, closable, closeLabel and dividerVisibility, the close reason close (BSH-28, BSH-29) — and the report of a sheet with no way out a person can see (BSH-30). TanreiBottomSheet(visible, onClose: (TanreiSheetCloseReason) -> Unit, variant, detent, onDetentChange, dismissible, handle, handleLabel, inset, contentDescription, initialFocus: FocusRequester?, content). visible rather than calling it only while open, so that the exit motion can run before the sheet goes; the caller sets visible to false from onClose. The sheet keeps a height the person chose until detent changes (BSH-21). Modal: drawn in a Dialog window of its own, as TanreiDialog and the modal drawer are, with usePlatformDefaultWidth = false, decorFitsSystemWindows = false and the window's own dim off (Tanrei draws the scrim); the window traps focus, answers back and hides the content behind from TalkBack; the sheet sets a pane title from contentDescription. Standard: fills its parent (a Box over the content) and draws the sheet at its bottom edge; the part above takes no touches. Drag with Modifier.draggable and nested scroll for BSH-10; fling and slop from ViewConfiguration. Snapshot harness: renders TanreiBottomSheetContent (the sheet and, for a modal sheet, the scrim) in place in a 400 × 720 dp box over a surface page; the window itself is covered by instrumented tests. Not built on this platform yet: the header — headline, closable, closeLabel and dividerVisibility, the close reason close (BSH-28, BSH-29) — and the report of a sheet with no way out a person can see (BSH-30). TanreiBottomSheet(variant:, open:, onClose: ValueChanged<TanreiSheetCloseReason>, detent:, onDetentChanged:, dismissible:, showHandle:, handleSemanticLabel:, inset:, semanticLabel:, child:), controlled: the caller sets open to false from onClose. The product places it in a Stack over what it covers — for a modal sheet, a Stack over the whole page, as the modal navigation drawer — not showModalBottomSheet, which needs Material. The modal form draws its scrim over the whole Stack, keeps focus in a FocusScope, blocks the page's semantics (BlockSemantics), names itself with Semantics(scopesRoute: true, namesRoute: true), answers back through PopScope and Escape through Shortcuts/Actions. Initial focus is autofocus: true on a focusable widget. Drag with a vertical drag recogniser; fling kMinFlingVelocity, slop kTouchSlop. Snapshot harness: a golden of the whole 400 × 720 surface with the sheet in a Stack over a surface page. Not built on this platform yet: the header — headline, closable, closeLabel and dividerVisibility, the close reason close (BSH-28, BSH-29) — and the report of a sheet with no way out a person can see (BSH-30). Controlled: open, onClose(reason), detent, onDetentChange; the caller sets open to false from onClose. Modal: presented in a transparent Modal with animationType="none", statusBarTranslucent and navigationBarTranslucent (Tanrei draws the scrim and the motion); onRequestClose (Android back, and Escape on a hardware keyboard) is close with the reason back or escape. Standard: a View positioned absolutely at the bottom of its parent. Drags with PanResponder (no gesture-handler dependency): slop 8 pt, fling 0.5 pt/ms. The on-screen keyboard is read from Keyboard events. Initial focus is an initialFocusRef the product passes, moved with AccessibilityInfo.setAccessibilityFocus. Insets come from the sheet's own insets prop, falling back to TanreiProvider's insets; the package takes no safe-area dependency. Snapshot harness: renders BottomSheetSurface (the sheet and, for a modal sheet, the scrim) in place in a 400 × 720 view over a surface page, since a Modal is a native window the snapshot does not include. React Native cannot hand one gesture from the sheet to the content's native scroll view partway through, so BSH-10's hand-off takes two gestures there; the package takes no gesture-handler dependency (as it takes no safe-area one). Not built on this platform yet: the header — headline, closable, closeLabel and dividerVisibility, the close reason close (BSH-28, BSH-29) — and the report of a sheet with no way out a person can see (BSH-30).

Tokens

Token Alias de Valor resuelto (claro)
--tanrei-comp-bottom-sheet-close-button-variant — 1
--tanrei-comp-bottom-sheet-container-color sys.color.surface-container-low #f9f9f9
--tanrei-comp-bottom-sheet-container-elevation sys.elevation.level2 0px 0px 0px 1px rgb(0 0 0 / 0.08), 0px 1px 2px 0px rgb(0 0 0 / 0.04)
--tanrei-comp-bottom-sheet-container-margin-block-start sys.space.space600 48px
--tanrei-comp-bottom-sheet-container-max-width — 640px
--tanrei-comp-bottom-sheet-container-padding sys.space.space200 16px
--tanrei-comp-bottom-sheet-container-partial-fraction — 0.5
--tanrei-comp-bottom-sheet-container-shape sys.shape.corner.large 12px
--tanrei-comp-bottom-sheet-content-color sys.color.on-surface #1b1b1b
--tanrei-comp-bottom-sheet-divider-color sys.color.outline-variant #000000 · 8%
--tanrei-comp-bottom-sheet-divider-visibility sys.behavior.divider-visibility 0
--tanrei-comp-bottom-sheet-divider-width sys.border.hairline 1px
--tanrei-comp-bottom-sheet-drag-handle-container-color comp.handle.standard.container.color #000000 · 0%
--tanrei-comp-bottom-sheet-drag-handle-focused-container-color — #848484 · 10%
--tanrei-comp-bottom-sheet-drag-handle-focused-grip-color — #848484
--tanrei-comp-bottom-sheet-drag-handle-focused-state-layer-color — #848484
--tanrei-comp-bottom-sheet-drag-handle-grip-color comp.handle.standard.line.color #848484
--tanrei-comp-bottom-sheet-drag-handle-hovered-container-color — #848484 · 8%
--tanrei-comp-bottom-sheet-drag-handle-hovered-grip-color — #4b4b4b
--tanrei-comp-bottom-sheet-drag-handle-hovered-state-layer-color — #848484
--tanrei-comp-bottom-sheet-drag-handle-pressed-container-color — #848484 · 10%
--tanrei-comp-bottom-sheet-drag-handle-pressed-grip-color — #4b4b4b
--tanrei-comp-bottom-sheet-drag-handle-pressed-state-layer-color — #848484
--tanrei-comp-bottom-sheet-drag-handle-state-layer-color comp.bottom-sheet.drag-handle.grip.color #848484
--tanrei-comp-bottom-sheet-focused-opacity — 1
--tanrei-comp-bottom-sheet-handle-container-height comp.handle.container.breadth.medium 24px
--tanrei-comp-bottom-sheet-handle-container-shape comp.handle.container.shape 9999px
--tanrei-comp-bottom-sheet-handle-container-width comp.handle.container.length.medium 48px
--tanrei-comp-bottom-sheet-handle-grip-height comp.handle.line.thickness.medium 4px
--tanrei-comp-bottom-sheet-handle-grip-width comp.handle.line.length.medium 32px
--tanrei-comp-bottom-sheet-handle-margin-block sys.space.space150 12px
--tanrei-comp-bottom-sheet-header-gap sys.space.space100 8px
--tanrei-comp-bottom-sheet-header-height — 64px
--tanrei-comp-bottom-sheet-header-padding-inline-end sys.space.space150 12px
--tanrei-comp-bottom-sheet-headline-color sys.color.on-surface #1b1b1b
--tanrei-comp-bottom-sheet-hovered-opacity — 1
--tanrei-comp-bottom-sheet-motion-enter-duration sys.motion.role.appear.sheet.enter 400ms
--tanrei-comp-bottom-sheet-motion-enter-easing sys.motion.easing.enter cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-bottom-sheet-motion-exit-duration sys.motion.role.appear.sheet.exit 350ms
--tanrei-comp-bottom-sheet-motion-exit-easing sys.motion.easing.exit cubic-bezier(0.3, 0, 1, 1)
--tanrei-comp-bottom-sheet-motion-feedback-duration comp.handle.motion.feedback-duration 450ms
--tanrei-comp-bottom-sheet-motion-state-duration comp.handle.motion.state-duration 200ms
--tanrei-comp-bottom-sheet-motion-state-easing comp.handle.motion.state-easing cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-bottom-sheet-pressed-opacity — 1
--tanrei-comp-bottom-sheet-scrim-blur comp.modal-layer.scrim.blur 0px
--tanrei-comp-bottom-sheet-scrim-color comp.modal-layer.scrim.color #000000 · 40%
--tanrei-comp-bottom-sheet-headline-typescale sys.typescale.heading-5 500 16px/1.5

Fixtures

Representaciones canónicas. Todas las plataformas capturan cada fixture con el mismo identificador para poder comparar los resultados.

Generado a partir de specs/bottom-sheet.spec.json, tokens/comp/bottom-sheet.tokens.json, design/tokens/

Pautas

Cuándo usarlo

Usa una hoja inferior en una pantalla compacta, donde no hay sitio junto al contenido, para algo que pertenece a la pantalla pero no es su tema principal: los lugares cercanos a un punto de un mapa, las formas de compartir una foto, una elección o un formulario breves. La hoja sube desde el borde que queda más cerca del pulgar.

Usa una hoja lateral para el mismo tipo de contenido en una ventana ancha, donde puede situarse junto al contenido en lugar de encima. Usa un diálogo para una decisión sin la cual la tarea no puede continuar. Para los destinos de un producto usa un panel de navegación, no una hoja inferior.

Varias acciones a partir de la acción principal. Cuando la acción principal de una pantalla se presenta en unas pocas formas relacionadas (un documento nuevo, una carpeta o una subida), el botón de acción principal abre una hoja inferior modal que las enumera. En el sistema Tanrei esto ocupa el lugar del menú FAB, que este sistema no incluye.

Varias acciones a partir de la acción principal: abre la hoja con el botón

addDocumentoCarpetaSubir un archivo
Forma Qué es Úsala para
Modal (predeterminada) Una superposición modal. La hoja sube deslizándose sobre un velo que atenúa toda la ventana. Todo lo que queda fuera está inerte, y el foco permanece dentro hasta que se cierra. Un paso que la persona termina o descarta antes de volver: compartir, una elección, un formulario breve.
Estándar Una región del diseño de página. La hoja cubre la parte inferior de su padre sin velo, y el contenido que hay encima sigue siendo utilizable. Tab se mueve entre la hoja y el contenido, y que el foco salga de la hoja nunca la cierra. Contenido que la persona usa junto con la pantalla, como una lista de lugares sobre un mapa.

Ambas formas leen los mismos tokens: solo las distinguen el velo y el comportamiento, así que un sistema de diseño no les da aspectos diferentes.

Las regiones del diseño de página son un panel de navegación estándar (incluida su forma contraída), una barra de la aplicación y una hoja lateral estándar, todas ellas situadas junto al contenido o encima de él. Una hoja inferior estándar, en cambio, cubre parte de su padre, pero también es una región del diseño de página, porque lo que importa es el comportamiento: no es modal, no dibuja ningún velo y el contenido sigue siendo utilizable mientras está abierta. Una hoja que se cerrara cada vez que el foco saliera de ella no podría usarse con el teclado junto al mapa que describe.

La hoja estándar no mueve lo que cubre ni le añade margen interior. Si el final de tu contenido con desplazamiento debe seguir visible por encima de la hoja, añade a ese contenido un margen interior igual a la altura parcial de la hoja.

Alturas

detent tiene dos valores. En partial, la hoja ocupa una parte del área que cubre: la mitad con los tokens predeterminados. En full, llega hasta un pequeño margen por debajo del borde superior del área. El área es la ventana por debajo de su margen interior superior en una hoja modal, y el padre en una estándar. Mientras se muestra el teclado en pantalla, el área termina en el teclado, de modo que la hoja se apoya en él.

La hoja solo es tan alta como su contenido. Una hoja cuyo contenido cabe en la altura parcial tiene una sola altura, y detent no cambia nada. Mientras el contenido es más alto, la hoja tiene dos alturas, y la persona pasa de una a otra arrastrando o con el tirador de arrastre. Cada uno de esos cambios emite detentChange con la nueva altura. En la web, Compose, Flutter y React Native, la hoja conserva la altura que eligió la persona hasta que vuelves a definir detent. En SwiftUI escribe la altura en el binding detent. Definir detent tú mueve la hoja sin emitir nada.

El tirador de arrastre y los gestos

El tirador de arrastre es la barra corta de la parte superior de la hoja, en una fila que permanece en su sitio mientras el contenido se desplaza. Con handle activado, se dibuja en todas las hojas.

  • Un control, cuando hay adónde ir. Mientras la hoja tiene dos alturas, el tirador es un botón que recibe su nombre de handleLabel, con el estado expandido en la altura completa. Una pulsación, Enter o Espacio llevan la hoja a su otra altura. Mientras la hoja tiene una sola altura, el tirador se dibuja en reposo como señal de que la hoja se puede arrastrar, pero no puede recibir el foco ni pulsarse, y la tecnología de apoyo no lo ve.
  • Su objetivo. Toda la fila a lo ancho del tirador es su área de pulsación, como mínimo el objetivo mínimo en cada dirección, aunque la barra en sí es pequeña.
  • Arrastrar. La persona puede arrastrar la hoja hacia arriba y hacia abajo por la fila del tirador, por el margen interior que rodea el contenido o por el propio contenido. La hoja sigue al dedo o al puntero una vez que este ha superado el umbral de arrastre táctil de la plataforma (8 puntos donde la plataforma no define ninguno), y nunca sube por encima de su altura completa.
  • Soltar. Al soltar, un lanzamiento asienta la hoja en la siguiente altura en su dirección; si se suelta más despacio, la hoja se asienta en la altura más cercana. Hay lanzamiento cuando se suelta a más velocidad que la velocidad mínima de lanzamiento de la plataforma, o que 0.5 puntos por milisegundo donde la plataforma no define ninguna.
  • Arrastrar para cerrar. Soltar por debajo de la mitad de la altura más baja, o lanzar hacia abajo desde la altura más baja, pide a la hoja que se cierre con el motivo drag, mientras dismissible sea true. La hoja se mantiene durante un fotograma donde la dejó el arrastre, y después sale deslizándose si pusiste open en false, o vuelve a asentarse si no lo hiciste. Mientras dismissible es false, se asienta en su altura más baja.
  • Contenido que se desplaza. Un arrastre hacia arriba sobre el contenido primero sube la hoja a la altura completa y después desplaza el contenido. Un arrastre hacia abajo primero desplaza el contenido de vuelta a su inicio y después baja la hoja. Donde una plataforma no puede pasar un mismo gesto de la hoja al desplazamiento del contenido, la segunda parte requiere un segundo arrastre (consulta más abajo las carencias conocidas por plataforma). El teclado y la tecnología de apoyo pueden desplazar el contenido en cualquiera de las dos alturas.

Arrastrar nunca es la única manera. El tirador cambia la altura con una sola pulsación o desde el teclado, y una hoja modal también se cierra con el velo, con Escape o con la acción de retroceso. Una hoja estándar, y una modal cuyo dismissible es false, no tienen esa segunda manera de cerrarse, así que dales un botón que las cierre.

Apertura, foco y cierre

  • Apertura. La hoja sube deslizándose desde debajo del borde inferior. Una hoja modal hace aparecer al mismo tiempo su velo, con un fundido, sobre toda la ventana. Una hoja que se muestra con open ya en true aparece en su sitio, sin movimiento. Una estándar deja entonces el foco donde está. Una modal lleva el foco a su interior como si acabara de abrirse, porque ya nada de lo que hay detrás puede recibir el foco.
  • Entrada del foco. Al abrirse, el foco pasa al elemento que marcaste, o a la hoja en sí, en ambas formas. El foco se mueve cuando la hoja se abre, no cuando termina su movimiento.
  • Retención del foco, solo en la modal. En una hoja modal, Tab desde el último elemento que puede recibir el foco va al primero, y Mayús+Tab desde el primero va al último. La navegación del lector de pantalla se queda dentro, y el contenido de detrás no se desplaza. Una hoja estándar no retiene nada: Tab continúa hacia el contenido, y la hoja sigue abierta.
  • Cierre. Cuando la hoja se cierra mientras el foco está dentro de ella, el foco vuelve al elemento que lo tenía cuando se abrió la hoja.
  • Apilamiento. Un diálogo, un menú u otra hoja modal abiertos desde una hoja modal se apilan encima de ella. Escape descarta solo la superior, y la hoja recupera el foco que esa superposición devuelve.
  • Movimiento. Donde el movimiento está reducido, la hoja aparece y desaparece con un fundido en su sitio, sin deslizarse. En Android, cuyo ajuste elimina todas las animaciones, aparece y desaparece de inmediato.

Recomendado

  • Deja a la vista la tarea de la hoja: un título en la parte superior del contenido, y un botón «Listo» o «Cerrar» cuando la hoja no tenga otra manera de cerrarse.
  • Desactiva inset para una lista cuyas filas llevan su propio margen interior, para que las filas lleguen hasta los bordes.
  • Deja el tirador activado en toda hoja cuyo contenido pueda crecer por encima de su altura parcial, para que quienes usan el teclado o un pulsador puedan cambiar su altura.
  • Usa la forma estándar cuando la persona necesite el contenido que hay encima de la hoja mientras esta está abierta.

Evita

  • No pongas dismissible en false salvo que la persona deba terminar la tarea de la hoja con uno de sus propios controles.
  • No uses una hoja modal para una decisión que deba interrumpir a la persona. Usa un diálogo.
  • No pongas la navegación de un producto en una hoja inferior. Usa un panel de navegación o una barra de navegación.
  • No apiles una hoja sobre otra para una tarea con pasos. Cambia el contenido de una sola hoja.

Accesibilidad

Rol
dialog
Etiquetado
The sheet is named by label, or by its headline where label is empty: a modal dialog while variant is modal, a region while it is standard. The headline is a heading; the close button is named by closeLabel; the divider is decorative. The drag handle, while the sheet has two heights, is a button named by handleLabel with the expanded state (true at full height); at one height it is hidden. The grip is decorative.
Objetivo mínimo
El área de pulsación cumple el tamaño mínimo de objetivo aunque el tamaño visual sea menor. minimum 32px · pointer 44px · spacing 8px · touch 48px

Teclado

Teclas Acción
Tab / Shift+Tab Moves focus through the drag handle (while it is interactive), the close button (while closable is true) and the content's focusable elements, in that order. In an open modal sheet it wraps and never leaves the sheet; from a standard sheet it moves on to the content, and the sheet stays open.
Enter / Space On the drag handle, moves the sheet to its other height (partial or full). On the close button, asks the sheet to close (close, reason close).
Escape Asks an open modal sheet to close (close, reason escape) while dismissible is true; with stacked overlays, only the topmost. A standard sheet ignores it.

Notas

  • Dragging is never the only way (WCAG 2.5.7): the drag handle changes the height with a single press or from the keyboard, and a modal sheet closes with Escape, back or a press on the scrim. A standard sheet, and a modal one whose dismissible is false, closes only by a drag, its close button or the product: set closable, or put a Close or Done button in its content.
  • The standard form is a layout region, although it covers the bottom of the content rather than pushing it aside: the person works in the content and in the sheet at once, so focus leaving it never closes it. The modal form is a modal overlay.
  • Web: the modal form is a native <dialog> opened with showModal() inside the shadow root, so the rest of the page is inert and the sheet is in the top layer; ::backdrop draws the scrim. Its cancel event (Escape) is cancelled and becomes close with the reason escape; the product then sets open to false. The standard form is a region (a <section> named by label) positioned at the bottom of its containing block. The `autofocus` attribute on a slotted element marks initial focus.
  • Where a platform presents the modal sheet in a window of its own (Compose's Dialog, React Native's Modal), the window's own dim and transition are switched off and Tanrei draws the scrim and the motion, so every platform draws the same color.
  • The sheet does not move or pad the content a standard sheet covers; a product whose content must stay visible above the sheet pads its scrolling content by the sheet's partial height.
  • How a state looks is a value decision of the active design system; components read the state's tokens and never compute the effect themselves.

Generado a partir de specs/bottom-sheet.spec.json, tokens/sys/measure.tokens.json

De qué se encarga el componente

  • Una hoja modal se expone como un diálogo modal que recibe su nombre de label, o de su headline cuando label está vacío. Una hoja estándar es una región que recibe su nombre del mismo modo, una región de referencia (landmark) en la web.
  • El titular es un encabezado. El botón de cierre es un botón de solo icono de Tanrei que recibe su nombre de closeLabel, con el objetivo mínimo, el anillo de foco y los estados del propio botón; pide a la hoja que se cierre con el motivo close en ambas formas, diga lo que diga dismissible. El divisor bajo la cabecera es decorativo.
  • Mientras una hoja modal está abierta, todo lo que queda fuera está inerte y oculto para la tecnología de apoyo, y el foco y la navegación del lector de pantalla permanecen dentro. Una hoja estándar no deja nada inerte.
  • Al abrirse, el foco pasa al elemento que marcaste, o a la hoja. Al cerrarse, el foco vuelve al elemento que lo tenía cuando se abrió la hoja. Esto vale para ambas formas.
  • Escape, la acción de retroceso de la plataforma y una pulsación en el velo piden a una hoja modal que se cierre, mientras dismissible sea true. Una hoja estándar ignora Escape y la acción de retroceso.
  • El tirador de arrastre es un tirador de tamaño que siempre se dibuja y que toma los tokens propios de la hoja. Mientras la hoja tiene dos alturas, es un único botón, que puede recibir el foco y recibe su nombre de handleLabel, expandido en la altura completa. Enter y Espacio llevan la hoja a su otra altura. Con una sola altura, el tirador queda oculto para la tecnología de apoyo. La barra del tirador siempre está oculta.
  • El área de pulsación del tirador es como mínimo el objetivo mínimo, y su anillo de foco solo se muestra para el foco del teclado.
  • El contenido se desplaza dentro de la hoja, y el foco del teclado y la tecnología de apoyo pueden desplazarlo en cualquiera de las dos alturas.
  • Un texto más grande hace que la hoja crezca con su contenido, hasta la altura que permite su posición (detent), y a partir de ahí el contenido se desplaza. El tirador no se escala.
  • El color de la hoja llega hasta debajo del margen interior inferior del sistema, y su contenido queda por encima de ese margen y por encima del teclado en pantalla.
  • Las compilaciones de depuración avisan de una hoja sin label ni headline, de un tirador interactivo sin handleLabel, de un botón de cierre sin closeLabel, de una hoja sin tirador ni botón de cierre que aun así se puede descartar (nada en pantalla dice cómo) y de un valor desconocido de variant, detent o dividerVisibility.

Carencias conocidas por plataforma:

  • Un arrastre con el ratón sobre el contenido mueve la hoja como lo hace un toque, así que el texto de una hoja no se puede seleccionar arrastrando hacia abajo sobre él mientras la hoja todavía pueda moverse.
  • SwiftUI, Compose, Flutter y React Native todavía no dibujan la cabecera: headline, closable, closeLabel y dividerVisibility, y el motivo close, solo existen en la web por ahora.
  • Web: con el tacto, el navegador decide quién se queda con un gesto cuando este empieza, así que el traspaso entre la hoja y el desplazamiento del contenido requiere un segundo gesto. Un ratón o un lápiz hacen el traspaso dentro de un mismo arrastre. El arrastre táctil todavía no está cubierto por ninguna prueba. En Firefox la hoja se va de inmediato en lugar de salir deslizándose, igual que el diálogo.
  • SwiftUI no tiene rol de diálogo ni de región: la hoja es un contenedor que recibe su nombre de label, con el rasgo modal mientras es modal. El tirador no tiene estado expandido, así que su valor dice «expanded» o «collapsed», solo en inglés. SwiftUI no puede llevar el foco a la hoja en sí cuando no hay nada marcado, y al cerrarse no devuelve el foco del teclado a ningún elemento; en iOS, la cubierta modal devuelve el foco de VoiceOver. En macOS, una hoja modal cubre solo la vista a la que está aplicado el modificador, así que aplícalo a la raíz de la ventana.
  • SwiftUI en iOS: por debajo de su altura completa, un arrastre sobre el contenido sube la hoja y el siguiente arrastre desplaza, y ahí el contenido no se desplaza, ni siquiera para VoiceOver o el teclado. El texto simple del contenido toma la fuente de cuerpo sin su interlineado, así que dos líneas miden 40 puntos de alto donde las demás plataformas dibujan 44.
  • SwiftUI: el margen interior inferior y el teclado en pantalla, el teclado físico, Escape y el gesto de escape de iOS no se han ejecutado en un dispositivo ni se han probado pulsándolos.
  • Compose: la hoja desplaza su contenido ella misma, así que dale una columna simple, no una lista perezosa. Mientras una hoja modal se cierra, su ventana conserva el foco hasta que termina el movimiento de salida. Sus arrastres solo se han verificado con Robolectric, no en un dispositivo.
  • Flutter: una hoja estándar es un contenedor con nombre, no una región de referencia, porque Flutter 3.32 no tiene ese rol. La hoja todavía no se ha comprobado con TalkBack y VoiceOver en un dispositivo.
  • React Native no puede saber qué elemento tenía el foco, así que, cuando la hoja se cierra, el foco va adonde lo pone la plataforma, no de vuelta al elemento que la abrió, igual que con el diálogo y el panel de navegación. Sin initialFocusRef, al abrirse solo se mueve el lector de pantalla, hacia la hoja.
  • React Native no puede pasar un mismo gesto de la hoja al desplazamiento del contenido a mitad de camino, así que el resto de ese arrastre se pierde y el siguiente desplaza. El Modal, el arrastre, el ajuste para esquivar el teclado y los márgenes interiores solo se han verificado en Jest, nunca en un dispositivo.
  • Con cupertino, la barra del tirador se dibuja con el color de contorno claro de Apple y mide 1.8:1 frente a la hoja en el esquema claro y 2.2:1 en el oscuro, por debajo del 3:1 que necesita el límite de un control. Los esquemas de contraste medio y alto la llevan a 4.5:1 o más. Las dos cifras que no llegan al umbral son mediciones que se conservan a título informativo: los criterios de accesibilidad de Tanrei no se aplican a un preajuste de imitación.
  • Con m3-like, la hoja a altura completa conserva el margen superior del sistema predeterminado: el valor de Material espera a que la referencia de fidelidad cubra la hoja inferior.

Qué debes hacer tú

  • Dale a cada hoja un label o un headline, como «Guardar foto». Da nombre al diálogo o a la región.
  • Dale al tirador un handleLabel, como «Cambiar el tamaño de la hoja», en el idioma de tu producto.
  • Cierra la hoja tú: pon open en false en respuesta a close (el evento close en la web, onClose en Compose, Flutter y React Native), y desde tu propio botón «Listo» o «Cerrar». En SwiftUI, la hoja escribe false en el binding por ti.
  • Dale a una hoja estándar, y a una hoja modal cuyo dismissible es false, un botón visible que la cierre: su botón de cierre (closable, con un closeLabel) o uno tuyo. Arrastrar no debe ser la única salida.
  • Marca el foco inicial donde ayude, como en el primer campo de un formulario breve.
  • En React Native, pasa initialFocusRef, devuelve tú el foco al elemento que abrió la hoja después de que se cierre, y pasa los márgenes interiores de la ventana a insets de la hoja o a TanreiProvider.