Diálogo
Detiene a la persona para una decisión o unos pocos datos sin los que la tarea no puede continuar, en una superficie que atenúa y bloquea todo lo que hay detrás hasta que se responde o se descarta.
- Categoría
- Contención
- Tokens
comp.dialog- Nivel
- Compuesto
- Se compone de
- Modal layer
- Web
tanrei-dialogschedule - SwiftUI
tanreiDialogschedule - Jetpack Compose
TanreiDialogschedule - Flutter
TanreiDialogschedule - React Native
Dialogschedule
Generado a partir de docs/status/status.json, specs/dialog.spec.json
Una especificación revisada, que aún no está en todas las plataformas
Esta página describe el diálogo tal como lo define ahora la especificación: todo diálogo ofrece al menos una acción, las acciones son una fila a todo lo ancho de botones tonal colocados según su tarea, el icono va junto a un titular alineado al inicio y, en una ventana compacta, el diálogo se sitúa en la parte inferior. El componente web implementa la revisión, y las demostraciones de esta página son el componente web tal como se distribuye hoy; arriba sigue marcado como rezagado hasta que la revisión se haya examinado y se hayan registrado sus imágenes de referencia. SwiftUI, Compose, Flutter y React Native ofrecen la revisión anterior hasta que se pongan al día: el icono encima de un titular centrado, acciones de texto en una fila al final y en el orden dado, acciones opcionales y un diálogo centrado en todas las ventanas.
Una confirmación: ábrela con el botón
Un diálogo es un componente controlado. Lo abres definiendo open y lo cierras volviendo a poner open en false, desde el controlador de una acción o en respuesta a dismiss. Una acción nunca cierra el diálogo por sí sola, así que puedes validar antes, mostrar el progreso o mantenerlo abierto si hay un error.
El diálogo tiene un icono opcional, un titular, un texto de apoyo opcional, contenido propio opcional y sus acciones. Un diálogo es una conversación: siempre ofrece al menos una respuesta. Las acciones son botones de Tanrei, cada uno marcado como confirming (completa la tarea: Guardar, Eliminar, Permitir) o dismissive (se echa atrás: Cancelar, Ahora no). El diálogo las coloca según esas marcas, así que puedes darlas en cualquier orden, y dibuja como tonal toda acción cuya variant no hayas definido.
Da a width el valor small, medium o large para que el diálogo tenga un ancho fijo en lugar del ancho de su contenido. Un diálogo no tiene botón de cerrar: su salida sin respuesta es una acción de descarte, como Cancelar.
Marca el elemento que debe recibir el foco cuando se abre el diálogo 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 marca, el foco va al propio diálogo, de modo que su titular se lee primero.
Los ejemplos de Web y React muestran la especificación actual. SwiftUI, Compose, Flutter y React Native van rezagados: todavía ofrecen la revisión anterior del diálogo, que no tiene las marcas confirming ni dismissive ni el valor predeterminado tonal, así que sus ejemplos dan a cada acción la variante text y ponen la de confirmación en último lugar, y muestran esa revisión hasta que se pongan al día.
<tanrei-button id="discard-draft" variant="text">Descartar borrador</tanrei-button>
<!-- closedby="none" equivale a dismissible en false. -->
<tanrei-dialog headline="¿Descartar el borrador?"
supporting-text="El mensaje que estabas escribiendo se eliminará de este dispositivo.">
<!-- dismissive va al inicio y confirming al final. Sin variante, las dos son tonal. -->
<!-- autofocus marca el foco inicial: la acción menos destructiva. -->
<tanrei-button slot="actions" dismissive autofocus>Cancelar</tanrei-button>
<tanrei-button slot="actions" confirming>Descartar</tanrei-button>
</tanrei-dialog>
<!-- Un icono junto al titular y una acción con su propia variante. -->
<tanrei-dialog headline="¿Eliminar el proyecto?" alert>
<span slot="icon">delete</span>
<tanrei-button slot="actions" dismissive variant="text">Cancelar</tanrei-button>
<tanrei-button slot="actions" confirming variant="filled" destructive>Eliminar</tanrei-button>
</tanrei-dialog>
<!-- Un formulario: Cancelar junto a Guardar, y un ancho fijo. Un diálogo no tiene botón de cerrar. -->
<tanrei-dialog headline="Editar el nombre visible" width="small">
<tanrei-text-input label="Nombre visible" autofocus></tanrei-text-input>
<tanrei-button slot="actions" dismissive>Cancelar</tanrei-button>
<tanrei-button slot="actions" confirming>Guardar</tanrei-button>
</tanrei-dialog>
<script type="module">
import '@tanrei/web/components/dialog';
const dialog = document.querySelector('tanrei-dialog');
const cancel = dialog.querySelector('[dismissive]');
const discard = dialog.querySelector('[confirming]');
document.querySelector('#discard-draft').addEventListener('click', () => (dialog.open = true));
// Una acción nunca cierra el diálogo: lo cierras tú, desde su controlador.
cancel.addEventListener('click', () => (dialog.open = false));
discard.addEventListener('click', () => { discardDraft(); dialog.open = false; });
// Escape y una pulsación en el fondo emiten cancel, como en un <dialog> nativo;
// e.detail.reason dice cuál fue: escape, back o scrim.
// Después el diálogo se cierra solo, salvo que canceles el evento.
dialog.addEventListener('cancel', (e) => { if (unsaved()) e.preventDefault(); });
// close sigue a cada cierre.
dialog.addEventListener('close', onDialogClosed);
</script>import { Button, Dialog } from '@tanrei/react';
// Escape y el fondo cierran el diálogo por sí mismos (onCancel y luego onClose): mantén open sincronizado desde onClose.
<Dialog
open={open}
headline="¿Descartar el borrador?"
supportingText="El mensaje que estabas escribiendo se eliminará de este dispositivo."
onClose={() => setOpen(false)}
>
<Button slot="actions" dismissive onClick={() => setOpen(false)}>Cancelar</Button>
<Button slot="actions" confirming onClick={discard}>Descartar</Button>
</Dialog>import Tanrei
// Un modificador, como .alert y .sheet. El descarte escribe false en el binding.
TanreiButton("Descartar borrador", variant: .text) { showDiscard = true }
.tanreiDialog("¿Descartar el borrador?", isPresented: $showDiscard,
supportingText: "El mensaje que estabas escribiendo se eliminará de este dispositivo.",
actions: {
TanreiButton("Cancelar", variant: .text) { showDiscard = false }
TanreiButton("Descartar", variant: .text) { discardDraft(); showDiscard = false }
})import io.github.chnotchy.tanrei.components.TanreiButton
import io.github.chnotchy.tanrei.components.TanreiDialog
import io.github.chnotchy.tanrei.tokens.TanreiButtonVariant
// visible, en lugar de llamarlo solo mientras está abierto, para que pueda ejecutarse el movimiento de salida.
val cancel = remember { FocusRequester() }
TanreiDialog(
visible = open,
onDismissRequest = { open = false },
headline = "¿Descartar el borrador?",
supportingText = "El mensaje que estabas escribiendo se eliminará de este dispositivo.",
initialFocus = cancel,
actions = {
TanreiButton(text = "Cancelar", onClick = { open = false }, modifier = Modifier.focusRequester(cancel), variant = TanreiButtonVariant.Text)
TanreiButton(text = "Descartar", onClick = { discardDraft(); open = false }, variant = TanreiButtonVariant.Text)
},
)import 'package:tanrei/tanrei.dart';
// showTanreiDialog apila una ruta modal. Navigator.pop la cierra; un descarte completa el future con null.
final bool? discard = await showTanreiDialog<bool>(
context: context,
barrierDismissible: true, // el dismissible de la especificación: Escape, volver y el velo
builder: (BuildContext context) => TanreiDialog(
headline: '¿Descartar el borrador?',
supportingText: 'El mensaje que estabas escribiendo se eliminará de este dispositivo.',
actions: <Widget>[
TanreiButton(
variant: TanreiButtonVariant.text,
autofocus: true,
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancelar'),
),
TanreiButton(
variant: TanreiButtonVariant.text,
onPressed: () => Navigator.pop(context, true),
child: const Text('Descartar'),
),
],
),
);import { useRef, useState } from 'react';
import type { View } from 'react-native';
import { Button, Dialog } from '@tanrei/react-native';
const cancel = useRef<View>(null);
const [open, setOpen] = useState(false);
// Con los nombres del Modal de React Native: se controla con visible, y onRequestClose es el dismiss de la especificación.
<Dialog
visible={open}
headline="¿Descartar el borrador?"
supportingText="El mensaje que estabas escribiendo se eliminará de este dispositivo."
onRequestClose={() => setOpen(false)}
initialFocusRef={cancel}
actions={
<>
<Button ref={cancel} variant="text" onPress={() => setOpen(false)}>Cancelar</Button>
<Button variant="text" onPress={discard}>Descartar</Button>
</>
}
/>Un diálogo de alerta: una confirmación destructiva que se lee en cuanto se abre
Anatomía
| Parte | Descripción |
|---|---|
1 scrim | The modal layer's scrim over everything behind the dialog (MDL-1). It blocks presses and, when the dialog is dismissible, dismisses it when pressed. |
2 container | The raised surface that holds everything else: centred in the window, or, in a compact window, with the bottom sheet's appearance at its bottom edge (DLG-17). It is a dialog in both places: the bottom sheet's look, not its drag. Its width follows its widest part, or the size width names (DLG-27). |
3 header | The row at the top: the icon and the headline, and nothing else (DLG-26, DLG-28). By default the icon sits at the headline's start and both are aligned to the start; a system may put the icon above a centred headline instead (DLG-9). |
4 icon · opcional | A glyph that marks the kind of decision (a warning, a deletion), at the headline's start. |
5 headline | The question or statement the dialog is about, aligned to the start. It is the dialog's accessible name. |
6 scroll · opcional | The area between the header and the actions that holds the supporting text and the content, and scrolls them when the dialog does not fit (DLG-11). It reaches the container's inline edges whatever inline padding the container has, so the dividers cross the whole container and nothing overflows it; inside, it insets its parts by that same padding. |
7 supporting-text · opcional | What will happen, or what the person needs to know to decide. It is the dialog's accessible description. |
8 content · opcional | Whatever else the decision needs: a list to choose from, a field. The dialog insets it and hands it a content color; it does not lay it out. |
9 divider · opcional | Hairlines between the header and the scrolling content and between the content and the actions, drawn only while the content scrolls: for as long as it does in the default system, or only while content lies beyond each line under dividerVisibility scroll. |
10 actions | The answers: one or more Tanrei Buttons, each marked confirming or dismissive, laid out as a full-width Button group (a row that the actions share equally, or a column when they do not fit). A Button whose variant the product did not set is tonal in the default system. Required: a dialog asks something, so it always offers an answer. |
Propiedades
| Propiedad | Tipo | Valor predeterminado | Descripción |
|---|---|---|---|
open | boolean | false | Whether the dialog is presented. The product opens it, and closes it from an action's handler or in answer to dismiss. |
headline | string | "" | The question or statement ("Discard draft?"). Required: it is the accessible name. |
supportingText | string | "" | What will happen, or what the person needs to know. It is the accessible description. |
alert | boolean | false | Marks a dialog that interrupts with something urgent (a destructive confirmation, a lost connection): assistive technology reads its headline and supporting text as soon as it opens, and the web exposes it as an alertdialog. |
dismissible | boolean | true | Whether Escape, the platform's back and a press on the scrim ask to close the dialog (dismiss). Set it to false only when the person must choose one of the actions; give such a dialog a dismissive action. A dialog has no close button (DLG-28): content that needs a way out and nothing more gets a dismissive action (Cancel, or Close where there is nothing to cancel), or belongs in a side sheet or a bottom sheet. |
width | content | small | medium | large | "content" | What decides the container's width while the dialog is centred (DLG-7). content: as wide as its widest part, between comp.dialog.container.min-width and comp.dialog.container.max-width (DLG-7). small, medium, large: as wide as comp.dialog.width.small, comp.dialog.width.medium or comp.dialog.width.large (400, 560 and 800 in the default system) whatever it holds, for a form or content whose width should not follow its text (DLG-27). Never wider than the window less its margins; as a sheet in a compact window every value is placed as DLG-17 says. A structural choice drawn by code, not a token axis. |
compactPresentation | sheet | own | "sheet" | How the dialog is presented in a compact window, one narrower than comp.dialog.compact.window-width. sheet: with the bottom sheet's appearance at the window's bottom edge (DLG-17, DLG-18), as every dialog and every popover of Tanrei is there; it stays a dialog. own: centred in every window, as DLG-7 places it, for a dialog whose look must not change with the window (a short alert that should read as one). A structural choice drawn by code, not a token axis. |
dividerVisibility | auto | always | scroll | "auto" | When the dividers below the header and above the actions are drawn while the supporting text and the content scroll between them. always: both, for as long as that part scrolls, wherever it is scrolled to. scroll: each only while content lies beyond it — below the header while the part is not at its top, above the actions while more of it lies below. auto takes comp.dialog.divider.visibility, the design system's answer (always in the default system). Under every choice a dialog whose parts all fit draws none. |
Slots
| Slot | Descripción |
|---|---|
default | The custom content, below the supporting text. |
icon | A glyph at the headline's start (DLG-9). |
actions | The action items: Tanrei Buttons, one at least (DLG-19), each marked confirming or dismissive. The dialog places them (DLG-10) and gives those without a variant its own (DLG-20). A fixture fills it through its items. |
Eventos
| Evento | Descripción |
|---|---|
dismiss | The person asked to close the dialog without an action: Escape, the platform's back or a press on the scrim. Carries which of these it was, named escape, back or scrim (in the platform's casing). Emitted only while dismissible is true. |
Elementos
Los tipos de elemento que el componente repite. Un fixture enumera sus elementos en orden; cada plataforma nombra cada tipo según su propio estilo.
action
One answer at the foot of the dialog: a Tanrei Button, placed by the dialog as a full-width Button group places its items and given the dialog's variant unless the product set one.
| Propiedad | Tipo | Valor predeterminado | Descripción |
|---|---|---|---|
variant | string | "" | The Button's variant (filled, tonal, elevated, outlined, text, or one a system added). Empty, the default: the dialog gives it the variant comp.dialog.actions.variant names, tonal in the default system (DLG-20). Set it to give one action another look (a filled Delete beside a text Cancel); a Delete that destroys something also sets the Button's own destructive, which the dialog leaves as set (BTN-40). |
disabled | boolean | false | As the Button's disabled. A disabled action keeps its place and its share of the width. |
confirming | boolean | false | The action completes the task (OK, Save, Delete, Allow). Placed at the end of a row and at the top of a column (BGR-13, BGR-14). It decides the place only, never the look (BGR-19). |
dismissive | boolean | false | The action backs out of the task (Cancel, Not now, Keep editing). Placed at the start of a row and at the bottom of a column (BGR-13, BGR-14). It decides the place only, never the look (BGR-19). |
| Slot | Descripción |
|---|---|
label | The Button's label, as its default slot. A fixture item's content is this label. |
| Evento | Descripción |
|---|---|
click | The action was activated, as the Button's click. The dialog does not close by itself (DLG-6). |
| Especificación | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| Elemento | tanrei-button | TanreiButton | TanreiButton | TanreiButton | Button |
variant · Propiedad | variant | variant | variant | variant | variant |
disabled · Propiedad | disabled | disabled | enabled | onPressed | disabled |
confirming · Propiedad | confirming | tanreiGroupPlacement(.confirming) | Modifier.confirming() | TanreiButtonGroupItem.placement | confirming |
dismissive · Propiedad | dismissive | tanreiGroupPlacement(.dismissive) | Modifier.dismissive() | TanreiButtonGroupItem.placement | dismissive |
click · Evento | click | action | onClick | onPressed | onPress |
| Notas | A tanrei-button child with slot="actions": its label is its content. confirming and dismissive are boolean attributes that the dialog reads, as the button group does. A Button without a variant attribute takes the dialog's variant through the internal controller the group already gives its items (DLG-20); the attribute, once written, wins. | A TanreiButton in the actions: builder. The flags are the button group's .tanreiGroupPlacement(_:) modifier (one enum: both cannot be set). A Button created with variant: nil reads the dialog's variant from an environment value the dialog sets (internal to the package). | A TanreiButton in the actions lambda, whose receiver is the button group's scope, so the flags are its Modifier.confirming() and Modifier.dismissive(). enabled is disabled's inverse. variant: TanreiButtonVariant? = null takes the dialog's from a CompositionLocal (internal to the library). | A TanreiButton in actions:; a null onPressed disables it. The flags are the button group's TanreiButtonGroupItem(placement:, child:) (one enum: both cannot be set). A Button with variant: null takes the dialog's from an InheritedWidget (internal to the package). | A Button in the actions prop (an element or a fragment of them). confirming and dismissive are props the dialog reads from its actions, as the button group reads them. A Button with no variant takes the dialog's from a React context (internal to the package). |
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 |
|---|---|
DLG-1 | WHEN open becomes true THE dialog SHALL present its container on its modal layer (MDL-1, MDL-2): above everything in its window, with the layer's scrim in comp.dialog.scrim.color and comp.dialog.scrim.blur over the whole window behind it, everything outside it inert and the content behind it kept from scrolling, until it closes. |
DLG-2 | WHEN the dialog opens THE dialog SHALL move focus as its modal layer does (MDL-3): 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 the headline first and the next Tab reaches the first focusable element. |
DLG-3 | WHILE the dialog is open THE dialog SHALL keep focus inside it as its modal layer does (MDL-4): Tab and Shift+Tab wrap between its last and first focusable elements, and screen-reader navigation does not leave it. |
DLG-4 | WHEN the dialog closes THE dialog SHALL return focus and restore the content behind it as its modal layer does (MDL-2, MDL-5). |
DLG-5 | WHILE dismissible is true, WHEN the user presses Escape, uses the platform's back or presses the scrim, as its modal layer answers them (MDL-6), THE dialog SHALL emit dismiss once with the reason escape, back or scrim; the product closes it by setting open to false, except on the web, where the dialog closes itself unless dismiss is cancelled, as a native dialog does. WHILE dismissible is false THE dialog SHALL ignore these inputs and emit nothing. |
DLG-6 | WHEN an action is activated THE dialog SHALL NOT close by itself; the product closes it from the action's handler by setting open to false. |
DLG-7 | WHILE the dialog is not presented as a sheet (DLG-17) — in a window at least comp.dialog.compact.window-width wide, or while compactPresentation is own — THE dialog SHALL centre its container in the window's safe area, at least comp.dialog.container.margin from every edge and from the on-screen keyboard, as wide as width names (DLG-27) — WHILE width is content, as wide as its widest part (its text, its content, or its actions in one row at their own widths) between comp.dialog.container.min-width and comp.dialog.container.max-width — but never wider than the window less the margins. There THE dialog SHALL draw its container in comp.dialog.container.color with comp.dialog.container.elevation and comp.dialog.container.shape corners eased 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). |
DLG-8 | THE dialog SHALL inset its parts by comp.dialog.container.padding on every side and stack the header (the icon and the headline, DLG-9), the supporting text and the content comp.dialog.container.gap apart, leaving out a part that is empty, with the actions comp.dialog.actions.margin-block-start below the last of them. It SHALL draw the headline in comp.dialog.headline.typescale and comp.dialog.headline.color, wrapping onto more lines rather than truncating, and the supporting text in comp.dialog.supporting-text.typescale and comp.dialog.supporting-text.color, and SHALL hand the content comp.dialog.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); plain text given as content, where the platform must style it, SHALL take sys.typescale.body. THE dialog SHALL set the headline's letter-spacing by the script of its text, as every title is set (PRS-20): no negative letter-spacing WHERE its language is Chinese, Japanese or Korean, and none WHERE its script joins its letters or stacks marks on them. |
DLG-9 | WHILE the icon slot is filled THE dialog SHALL draw the icon at comp.dialog.icon.size in comp.dialog.icon.color, with the family and fill of an unselected icon, sys.icon.weight and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max, and place it as comp.dialog.icon.placement says: WHILE it is inline, at the start of the header, comp.dialog.header.gap before the headline and centred on the headline's first line, so that a headline that wraps keeps the icon beside its first line; WHILE it is above, above the headline, comp.dialog.container.gap apart, with the icon and the headline centred. The supporting text and the content SHALL be aligned to the start in either placement, and WHILE the icon slot is empty the headline SHALL be aligned to the start whatever the placement. |
DLG-10 | THE dialog SHALL lay out its actions as a standard Button group lays out its items (BGR-12 to BGR-19), comp.dialog.actions.gap apart, in the order the group gives them — dismissive at the start and confirming at the end of a row, confirming on top and dismissive at the bottom of a column — with focus and reading order following that order (BGR-15). WHILE comp.dialog.actions.arrangement is fill it SHALL make them a full-width group across the container's inner width, the actions sharing a row equally (BGR-17) or each as wide as the column; WHILE it is end it SHALL keep them at their own widths, the row aligned to the end, and a column as wide as its widest action and aligned to the end. It SHALL place them in a row WHILE every action's label fits its share on one line (fill) or the row at its own widths fits the inner width (end), and otherwise in a column, re-deciding when the dialog's width or the text size changes. |
DLG-11 | WHEN the dialog's parts do not fit in the height its placement leaves THE dialog SHALL keep the header and the actions in place and scroll the supporting text and the content between them, and SHALL draw a divider of comp.dialog.divider.width in comp.dialog.divider.color across the container below the header and another above the actions: WHILE its divider visibility (DLG-24) is always, both for as long as that part scrolls, wherever it is scrolled to, at its top included; WHILE it is scroll, the one below the header only WHILE the scrolled part is not at its top and the one above the actions only WHILE more of it lies below. WHILE everything fits THE dialog SHALL draw no divider. |
DLG-12 | WHEN the header and the actions alone leave no room for the scrolled part (large text on a short window) THE dialog SHALL scroll all of its parts together instead, so that nothing is cut off or unreachable. |
DLG-13 | WHEN the dialog opens centred in the window (DLG-7) THE dialog SHALL fade its scrim in with its container, as its modal layer says (MDL-7), and make its container appear as every modal surface appears: from sys.motion.appear.opacity, sys.motion.appear.scale, sys.motion.appear.distance and sys.motion.appear.blur to whole, grown about its centre and travelling up into place, over comp.dialog.motion.enter-duration with comp.dialog.motion.enter-easing; WHEN it closes there it SHALL take the container back the same way to where it started while fading the scrim out, over comp.dialog.motion.exit-duration with comp.dialog.motion.exit-easing. A motion turned round SHALL go on from the opacity and the size it has reached. WHERE sys.behavior.appearance is unfold the scale SHALL apply on both axes, from the centre: a centred container has no anchored edge. WHERE motion is reduced it SHALL only fade the container in and out, over sys.motion.role.reduced, with no scale, distance or blur, or appear and disappear at once on a platform whose reduced-motion setting removes every animation (Android's). WHEN it is mounted with open true THE dialog SHALL appear without its motion while the document has not yet been first painted, and with it when it is mounted after that. Focus SHALL move (DLG-2, DLG-4) when the dialog opens or closes, not when the motion ends. |
DLG-14 | THE dialog SHALL expose its container as its modal layer's modal dialog (MDL-10) — an alert dialog WHILE alert is true — named by headline and described by supportingText; WHEN alert is true and the dialog opens it SHALL have assistive technology read the headline and the supporting text at once; WHILE headline is empty it SHALL still open, and in debug builds SHALL report that it has no accessible name. |
DLG-15 | WHEN a dialog opens while another is open THE dialog SHALL stack above it as modal layers stack (MDL-8): with its own scrim, the first one inert as well, focus returned on closing to the element inside the first dialog that had it, and Escape dismissing only the topmost dialog. |
DLG-16 | WHEN the layout direction is right-to-left THE dialog SHALL mirror its layout — the icon at the right of the headline, the headline and the supporting text aligned to the right, and its actions placed as a Button group places them right to left (BGR-8: dismissive at the right, confirming at the left of a row; a column's order unchanged) — and SHALL NOT mirror its content itself: the content follows the layout direction. |
DLG-17 | WHILE the window is narrower than comp.dialog.compact.window-width (a compact window) and compactPresentation is sheet THE dialog SHALL present its container with the bottom sheet's appearance (BSH-7): at the window's bottom edge, as wide as the window but no wider than comp.bottom-sheet.container.max-width and centred in it, whatever width names, filled with comp.bottom-sheet.container.color, raised by comp.bottom-sheet.container.elevation, its top corners rounded by comp.bottom-sheet.container.shape eased by sys.shape.corner-smoothing and its bottom corners square, its color reaching under the bottom system inset and its parts above it, sitting on the on-screen keyboard while it is shown, and never taller than the window below its top system inset less comp.bottom-sheet.container.margin-block-start. Its parts SHALL keep their own tokens and layout (DLG-8 to DLG-12, DLG-20), inside comp.dialog.container.padding. It SHALL stay a dialog there — DLG-1 to DLG-6, DLG-8 to DLG-12 and DLG-14 to DLG-16 hold unchanged, its scrim the dialog's own — and SHALL draw no drag handle, follow no drag and close on no swipe, because it has one height and its exits are its actions, Escape, back and the scrim. WHILE compactPresentation is own THE dialog SHALL be centred in every window as DLG-7 says. WHEN the window's width crosses comp.dialog.compact.window-width, or compactPresentation changes, while the dialog is open THE dialog SHALL take the other presentation at once, without motion, keeping focus where it is. WHERE comp.dialog.compact.window-width is 0 no window is compact. WHEN compactPresentation names a value the dialog does not define THE dialog SHALL treat it as sheet and in debug builds report the unknown value. |
DLG-18 | WHEN the dialog opens as a sheet in a compact window (DLG-17) THE dialog SHALL fade its scrim in (MDL-7) while sliding its container up from below the window's bottom edge into its place, as a modal bottom sheet opens (BSH-1), over comp.bottom-sheet.motion.enter-duration with comp.bottom-sheet.motion.enter-easing; WHEN it closes there it SHALL fade the scrim out while sliding the container down out of view, 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). Focus SHALL move as DLG-13 says. |
DLG-19 | IF the dialog opens with no action THE dialog SHALL still open and lay out its other parts, and in debug builds SHALL report as an error that a dialog must offer at least one action; a platform whose API can require an action (a required, non-empty list or builder of actions) SHALL require it, and one that can count its actions only when it opens SHALL count them then. It SHALL report likewise a dialog whose actions are all disabled while dismissible is false, which leaves the person no way out. |
DLG-20 | WHILE an action's variant is empty THE dialog SHALL give that Button the variant comp.dialog.actions.variant names, through the channel a Button reads from the component that places it, so that the Button renders from that variant's tokens and states as it would with the variant set; WHILE an action's variant is set THE dialog SHALL leave it as set. THE dialog SHALL NOT change an action's size or tokens, and SHALL NOT change its look or emphasis for its confirming or dismissive (BGR-19). |
DLG-21 | THE dialog 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 dialog is shown THE dialog SHALL draw itself for the new direction at once, without a motion of its own, keeping its open state and its focus. |
DLG-22 | WHERE the layout direction is right-to-left THE dialog SHALL draw the glyph of its icon slot mirrored left to right exactly when the system's icon set marks that glyph's name as mirrored or the product forces mirroring for that use, and unmirrored when the product forbids it: the icon helper mirrors the glyph inside its own box, about the box's vertical centre line, changing neither its place, size, color and fill nor the order DLG-16 gives. THE dialog SHALL NOT mirror a glyph itself, and SHALL NOT mirror content the product gives as an image or a view of its own rather than by a glyph's name. |
DLG-23 | THE dialog SHALL lay out its headline and its supporting text as text of its own direction, isolated from the text around it: each takes the base direction of its first strong character, or the layout direction where it has none, so that a left-to-right word, a number with its sign or unit, a telephone number and an address keep their own order in a right-to-left layout and reorder nothing beside them; THE dialog SHALL still place each and align its lines by the layout direction, not by the text's own. |
DLG-24 | THE dialog SHALL draw its dividers (DLG-11) as its dividerVisibility names: WHILE dividerVisibility is auto, as comp.dialog.divider.visibility names, which follows sys.behavior.divider-visibility unless a design system sets the dialog apart; WHILE dividerVisibility is always or scroll, as that value names, whatever the tokens say. WHEN dividerVisibility, or the token it follows, changes THE dialog SHALL draw the other form at once, moving no part and keeping the scroll position and focus; WHEN dividerVisibility names a value the dialog does not define THE dialog SHALL treat it as auto and in debug builds report the unknown value. A platform that cannot draw a choice SHALL fall back to the first value, always, and state that in its README. |
DLG-26 | THE dialog SHALL draw its header as one row that holds the icon and the headline as DLG-9 places them, across the container's inner width, and nothing at its end; WHILE headline is empty and the icon slot is empty THE dialog SHALL draw no header, leaving it out as DLG-8 leaves out an empty part. |
DLG-27 | WHILE the dialog is centred (DLG-7) and width is small, medium or large THE dialog SHALL make its container as wide as comp.dialog.width.small, comp.dialog.width.medium or comp.dialog.width.large, whatever its parts would ask and not held between comp.dialog.container.min-width and comp.dialog.container.max-width, but never wider than the window's safe area less comp.dialog.container.margin on each side; its headline and supporting text SHALL wrap to that width, its content SHALL take it, and its actions SHALL be placed in a row or a column as DLG-10 decides at that width. WHILE width is content THE dialog SHALL be as wide as DLG-7 says. WHILE it is a sheet THE dialog SHALL place its container as DLG-17 says, whatever width names, because a narrow window leaves no room for a choice of width. WHEN width changes while the dialog is open THE dialog SHALL take the new width at once, without a motion of its own, keeping its scroll position and focus; WHEN width names a value the dialog does not define THE dialog SHALL treat it as content and in debug builds report the unknown value. |
DLG-28 | THE dialog SHALL draw no close button in its header: a dialog asks for a decision, and its way out without one is its dismissive action and, WHILE dismissible is true, Escape, the platform's back and the scrim (DLG-5). THE dialog SHALL have no prop that draws one; WHERE a platform can be given a property the dialog does not define and the product gives one that asks for a close button (the web's closable and close-label attributes), THE dialog SHALL draw nothing for it and in debug builds SHALL report that a dialog has no close button. |
DLG-29 | 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 dialog 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 dialog (DLG-14) — an id reference by the elements it names in the host's tree or a tree around it, a description after its own. WHILE headline is not empty it is the dialog's name, over a name given so; WHILE headline is empty, a name given so is the dialog'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 dialog 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-dialog | tanreiDialog | TanreiDialog | TanreiDialog | Dialog |
open · Propiedad | open | isPresented | visible | showTanreiDialog | visible |
headline · Propiedad | headline | headline | headline | headline | headline |
supportingText · Propiedad | supportingText | supportingText | supportingText | supportingText | supportingText |
alert · Propiedad | alert | alert | alert | alert | alert |
dismissible · Propiedad | closedby | dismissible | dismissible | barrierDismissible | dismissible |
width · Propiedad | width | width | width | width | width |
compactPresentation · Propiedad | compactPresentation | compactPresentation | compactPresentation | compactPresentation | compactPresentation |
dividerVisibility · Propiedad | dividerVisibility | dividerVisibility | dividerVisibility | dividerVisibility | dividerVisibility |
dismiss · Evento | cancel | isPresented | onDismissRequest | showTanreiDialog | onRequestClose |
| Notas | supportingText is the supporting-text attribute. dismissible is HTML's own closedby attribute: any (the default: Escape and a press on the backdrop) or none. cancel is a CustomEvent whose detail.reason is escape, back or scrim, cancelable as on a native <dialog>; close follows every close. There is no closable or close-label attribute (DLG-28): given one, the dialog draws nothing for it, and debug builds report it. width is the width attribute (content, small, medium, large), and compactPresentation the compact-presentation attribute (sheet, own). Slots: default, icon, actions. The actions are laid out by the button group's placement (BGR-13 to BGR-17) inside the shadow root: the dialog's shadow root uses manual slot assignment, assigns its icon and content children to their slots, and assigns each action to one of three action slots in the order of the orientation, so focus and the accessibility tree follow the placed order (BGR-15); CSS order is not used. Compact (DLG-17): a media query cannot read a custom property, so the dialog reads the resolved comp.dialog.compact.window-width through a probe and matches it with matchMedia('(width < …px)'), re-placing at once on change; the sheet's appearance is the shared one the popover draws too (src/primitives/compact-sheet.ts), read from comp.bottom-sheet. Debug builds (import.meta.env.DEV or the package's dev export) report a dialog opened with no assigned action (DLG-19). Snapshot harness: a 640 × 640 viewport, and 360 × 640 for the compact- fixtures, with the dialog opened by open; the screenshot is the viewport, since the dialog and its backdrop are in the top layer. dividerVisibility is the divider-visibility attribute (auto, always, scroll); auto reads --tanrei-comp-dialog-divider-visibility from the cascade with a style query, and where the browser has none the dialog writes the name it read on its <dialog> when it lays out. | Lagging: ships the earlier revision — the icon above a centred headline, text actions in a row at the end, optional actions, centred in every window, the 480 × 640 harness. The target: a view modifier, as .alert and .sheet are: .tanreiDialog(_ headline:, isPresented: Binding<Bool>, supportingText:, alert:, dismissible:, icon:, actions:, content:). Dismissal writes false to the binding, SwiftUI's idiom. It is presented with .fullScreenCover, with a clear presentation background and the system transition off (Tanrei draws the scrim and the motion); that presentation makes the rest of the app inaccessible to VoiceOver, as accessibilityViewIsModal does. Initial focus is .defaultFocus or an @AccessibilityFocusState the product passes; the iOS escape gesture is .accessibilityAction(.escape). actions: is a required builder; the dialog counts its subviews through a variadic view and reports an empty one in debug builds (DLG-19), and lays them out with the button group's Layout. Compact is the window's width (a GeometryReader at the presentation's root) against the token, not the horizontal size class, so every platform switches at the same width. Snapshot harness: TanreiDialogSurface in place in a 640 × 640 frame, and 360 × 640 for the compact- fixtures. Not yet: the headline's letter-spacing by the script of its text (DLG-8, PRS-20). Not built on this platform yet: dividerVisibility (auto, always, scroll; DLG-24) — the dividers still follow the scroll position, as scroll does. Not built on this platform yet: width (DLG-27), and the reason dismiss carries. Not built on this platform yet: DLG-17 and DLG-18 as they are written now — the bottom sheet's appearance and motion in a compact window — and compactPresentation. | Lagging: ships the earlier revision — the icon above a centred headline, text actions in a row at the end, optional actions, centred in every window. The target: TanreiDialog(visible, onDismissRequest, headline, supportingText, alert, dismissible, initialFocus: FocusRequester?, icon, actions, content) with actions a required parameter; the group's Layout sees its measurables and reports none in debug builds (DLG-19). visible rather than calling it only while open, so that the exit motion can run before the window goes. Presented in a Dialog window with usePlatformDefaultWidth = false and the window's own dim off (Tanrei draws the scrim). dismissible sets dismissOnBackPress and dismissOnClickOutside. The headline is the window's paneTitle. Compact is LocalWindowInfo.current.containerSize against the token, not a window size class, so every platform switches at the same width. Snapshot harness: renders TanreiDialogContent (the surface and the scrim) in place in a 640 × 640 dp box, and 360 × 640 for the compact- fixtures; the window itself is covered by instrumented tests. Not yet: the headline's letter-spacing by the script of its text (DLG-8, PRS-20). Not built on this platform yet: dividerVisibility (auto, always, scroll; DLG-24) — the dividers still follow the scroll position, as scroll does. Not built on this platform yet: width (DLG-27), and the reason dismiss carries. Not built on this platform yet: DLG-17 and DLG-18 as they are written now — the bottom sheet's appearance and motion in a compact window — and compactPresentation. | Lagging: ships the earlier revision — the icon above a centred headline, text actions in a row at the end, optional actions, centred in every window, the 480 × 640 golden. The target: showTanreiDialog<T>(context:, barrierDismissible:, builder:) pushes a modal route that draws the scrim as its barrier, and returns a Future<T?>; Navigator.pop(context, value) from an action closes it, and a dismissal completes the future with null. TanreiDialog(headline:, supportingText:, alert:, icon:, required actions:, child:) is the surface the builder returns; actions is asserted non-empty (DLG-19). Initial focus is autofocus: true on a focusable widget. The route is marked scopesRoute and namesRoute, so assistive technology stays inside it. Compact is MediaQuery.sizeOf(context).width against the token. Snapshot harness: a golden of the whole 640 × 640 surface after showTanreiDialog, and 360 × 640 for the compact- fixtures, as the overlay is part of the widget tree. Not yet: the headline's letter-spacing by the script of its text (DLG-8, PRS-20). Not built on this platform yet: dividerVisibility (auto, always, scroll; DLG-24) — the dividers still follow the scroll position, as scroll does. Not built on this platform yet: width (DLG-27), and the reason dismiss carries. Not built on this platform yet: DLG-17 and DLG-18 as they are written now — the bottom sheet's appearance and motion in a compact window — and compactPresentation. | Lagging: ships the earlier revision — the icon above a centred headline, text actions in a row at the end, optional actions, centred in every window, the 480 × 640 harness. The target: named as React Native's Modal: visible and onRequestClose (Android's back and, here, Escape on a hardware keyboard and a press on the scrim). Presented in a transparent Modal with animationType="none" (Tanrei draws the scrim and the motion), statusBarTranslucent and navigationBarTranslucent. Initial focus is an initialFocusRef the product passes, moved with AccessibilityInfo.setAccessibilityFocus. actions is required in the types, and development builds report it empty when the dialog becomes visible (DLG-19). Compact is useWindowDimensions().width against the token. Snapshot harness: renders DialogSurface (the surface and the scrim) in place in a 640 × 640 view, and 360 × 640 for the compact- fixtures, since a Modal is a native window the snapshot does not include. Not yet: the headline's letter-spacing by the script of its text (DLG-8, PRS-20). Not built on this platform yet: dividerVisibility (auto, always, scroll; DLG-24) — the dividers still follow the scroll position, as scroll does. Not built on this platform yet: width (DLG-27), and the reason dismiss carries. Not built on this platform yet: DLG-17 and DLG-18 as they are written now — the bottom sheet's appearance and motion in a compact window — and compactPresentation. |
Tokens
| Token | Alias de | Valor resuelto (claro) |
|---|---|---|
--tanrei-comp-dialog-actions-arrangement | — | 0 |
--tanrei-comp-dialog-actions-gap | sys.space.space200 | 16px |
--tanrei-comp-dialog-actions-margin-block-start | sys.space.space300 | 24px |
--tanrei-comp-dialog-actions-variant | — | 1 |
--tanrei-comp-dialog-compact-window-width | — | 600px |
--tanrei-comp-dialog-container-color | sys.color.surface-bright | |
--tanrei-comp-dialog-container-elevation | sys.elevation.level3 | 0px 0px 0px 1px rgb(0 0 0 / 0.08), 0px 1px 1px 0px rgb(0 0 0 / 0.04), 0px 4px 8px -4px rgb(0 0 0 / 0.04), 0px 16px 24px -8px rgb(0 0 0 / 0.06) |
--tanrei-comp-dialog-container-gap | sys.space.space200 | 16px |
--tanrei-comp-dialog-container-margin | sys.space.space200 | 16px |
--tanrei-comp-dialog-container-max-width | — | 560px |
--tanrei-comp-dialog-container-min-width | — | 280px |
--tanrei-comp-dialog-container-padding | sys.space.space300 | 24px |
--tanrei-comp-dialog-container-shape | sys.shape.corner.large | 12px |
--tanrei-comp-dialog-content-color | sys.color.on-surface | |
--tanrei-comp-dialog-divider-color | sys.color.outline-variant | |
--tanrei-comp-dialog-divider-visibility | sys.behavior.divider-visibility | 0 |
--tanrei-comp-dialog-divider-width | sys.border.hairline | 1px |
--tanrei-comp-dialog-header-gap | sys.space.space150 | 12px |
--tanrei-comp-dialog-headline-color | sys.color.on-surface | |
--tanrei-comp-dialog-icon-color | sys.color.secondary | |
--tanrei-comp-dialog-icon-placement | — | 0 |
--tanrei-comp-dialog-icon-size | sys.icon.medium | 24px |
--tanrei-comp-dialog-motion-enter-duration | comp.modal-layer.motion.enter-duration | 350ms |
--tanrei-comp-dialog-motion-enter-easing | comp.modal-layer.motion.enter-easing | cubic-bezier(0.2, 0, 0, 1) |
--tanrei-comp-dialog-motion-exit-duration | comp.modal-layer.motion.exit-duration | 300ms |
--tanrei-comp-dialog-motion-exit-easing | comp.modal-layer.motion.exit-easing | cubic-bezier(0.3, 0, 1, 1) |
--tanrei-comp-dialog-scrim-blur | comp.modal-layer.scrim.blur | 0px |
--tanrei-comp-dialog-scrim-color | comp.modal-layer.scrim.color | |
--tanrei-comp-dialog-supporting-text-color | sys.color.on-surface-variant | |
--tanrei-comp-dialog-width-large | — | 800px |
--tanrei-comp-dialog-width-medium | — | 560px |
--tanrei-comp-dialog-width-small | — | 400px |
--tanrei-comp-dialog-headline-typescale | sys.typescale.heading-5 | 500 16px/1.5 |
--tanrei-comp-dialog-supporting-text-typescale | sys.typescale.body | 400 14px/1.5714 |
Fixtures
Representaciones canónicas. Todas las plataformas capturan cada fixture con el mismo identificador para poder comparar los resultados.
basic
open: true headline: Discard draft? supportingText: The message you were writing will be del… items: 2 Dibujado por separado en una ventana de 640 × 640icon
open: true headline: Delete project? supportingText: Everything in the project, including its… items: 2 slots: icon Dibujado por separado en una ventana de 640 × 640content
open: true headline: Share with the team supportingText: People you add can view and comment. items: 2 Dibujado por separado en una ventana de 640 × 640scrolling
open: true headline: Terms of service supportingText: Please read these terms before you conti… items: 2 Dibujado por separado en una ventana de 640 × 640single-action
open: true headline: Your session has expired supportingText: Sign in again to keep working. Nothing y… items: 1 Dibujado por separado en una ventana de 640 × 640stacked
open: true headline: Allow access to your location? supportingText: Nearby stores and delivery times use you… items: 3 Dibujado por separado en una ventana de 640 × 640action-variants
open: true headline: Delete 3 photos? supportingText: They are removed from every device signe… items: 2 Dibujado por separado en una ventana de 640 × 640width-small
open: true headline: Discard draft? supportingText: The message you were writing will be del… width: small items: 2 Dibujado por separado en una ventana de 640 × 640width-large
open: true headline: Share with the team supportingText: People you add can view and comment. width: large items: 2 Dibujado por separado en una ventana de 640 × 640compact
open: true headline: Discard draft? supportingText: The message you were writing will be del… items: 2 Dibujado por separado en una ventana de 360 × 640compact-stacked
open: true headline: Allow access to your location? supportingText: Nearby stores and delivery times use you… items: 3 slots: icon Dibujado por separado en una ventana de 360 × 640compact-own
open: true headline: Discard draft? supportingText: The message you were writing will be del… compactPresentation: own items: 2 Dibujado por separado en una ventana de 360 × 640Generado a partir de specs/dialog.spec.json, tokens/comp/dialog.tokens.json, design/tokens/
Usa un diálogo cuando la tarea no pueda continuar sin una respuesta: confirmar algo que no se puede deshacer, pedir un permiso o recoger dos o tres datos sin salir de la página. Detiene a la persona, así que úsalo solo cuando detenerla merezca la pena.
Usa un snackbar para informar de lo que acaba de ocurrir, con un «Deshacer» opcional, en lugar de preguntar «¿Seguro?» por algo que se puede deshacer. Usa un menú para una elección breve que no bloquea la pantalla. Lleva los formularios más largos y las tareas complejas a una página propia.
| Parte | Se define con | Notas |
|---|---|---|
| Icono | el slot de icono | Opcional. Un glifo que marca el tipo de decisión, como delete. Va al inicio de la primera línea del titular, 12 antes de ella, y se queda junto a esa línea cuando el titular salta de línea. |
| Titular | headline |
Obligatorio. La pregunta o la afirmación, como «¿Descartar el borrador?». Siempre alineado al inicio. Es el nombre accesible del diálogo. |
| Texto de apoyo | supportingText |
Qué va a ocurrir, o lo que la persona necesita saber para decidir. Siempre alineado al inicio. Es la descripción del diálogo. |
| Contenido | el slot predeterminado | Cualquier otra cosa que la decisión necesite: una lista para elegir, un campo. El diálogo le da un margen interior y un color de contenido; no lo dispone. |
| Acciones | el slot de acciones | Obligatorio: al menos un botón de Tanrei, cada uno marcado como confirming o dismissive. El diálogo los coloca como un grupo de botones a todo lo ancho y dibuja como tonal los que no tienen variante. |
Cuando las partes no caben en la ventana, la cabecera y las acciones se quedan en su sitio y el texto de apoyo y el contenido se desplazan entre ellos. Un divisor de línea fina separa de ellos la cabecera, y otro las acciones, mientras se desplazan, también cuando están en su inicio. Define dividerVisibility como scroll (divider-visibility en la web) para dibujar cada divisor solo mientras algo queda desplazado fuera de la vista más allá de él; cupertino y m3-like lo eligen para todos los diálogos. Cuando el texto grande no deja espacio ni siquiera para eso, se desplaza el diálogo entero, sin divisores.
En la web, la parte que se desplaza es ::part(scroll). Sea cual sea el margen interior horizontal que le des a ::part(container), esa parte llega hasta los bordes del contenedor y conserva ese mismo margen por dentro, así que sus divisores siguen cruzando todo el diálogo y nada se sale de él. Si tu contenido mantiene una fila fija al pie con position: sticky, indica a la parte que se desplaza cuánto mide esa fila, para que un campo que se desplaza a la vista al recibir el foco no quede debajo de ella:
tanrei-dialog::part(scroll) { scroll-padding-block-end: 56px; }Se admite un titular sin texto de apoyo: una pregunta en el titular, con sus respuestas, es una conversación. Lo que un diálogo no puede ser es una afirmación sin nada que responder.
Las acciones se reparten a partes iguales el ancho interior del contenedor, con 16 entre ellas.
- Colocadas según la tarea. En una fila, la acción de descarte está al inicio y la de confirmación al final. En una columna, la de confirmación está arriba y la de descarte abajo. Una acción sin ninguna de las dos marcas conserva el lugar que le diste, entre ambas. El foco y los lectores de pantalla siguen el orden de colocación, así que nunca ordenas los botones a mano.
- Fila o columna según quepan. Las acciones se quedan en una fila mientras cada etiqueta quepa en su parte en una sola línea. Cuando alguna tendría que saltar de línea, se apilan en una columna, cada una tan ancha como el diálogo. El diálogo vuelve a decidirlo cuando cambia su ancho o el tamaño del texto.
- Tonal de forma predeterminada. Una acción cuya
variantno hayas definido se dibuja como tonal. Las marcas solo deciden el lugar, nunca el aspecto. - Otro aspecto. Define la
variantde un botón para darle otro aspecto a esa acción, como un «Cancelar» text junto a un «Eliminar» filled. Definedestructiveen una acción que elimina o descarta, como ese «Eliminar»: conserva su variante y toma el color de error. El diálogo nunca sobrescribe una variante que hayas definido, nidestructive. Un producto que quiera otro aspecto para todos los diálogos lo define una sola vez en su capa (actions.variant, yactions.arrangementpara acciones con su propio ancho alineadas al final), no en cada botón. - Al menos una. Un diálogo sin ninguna acción se abre igualmente, y las compilaciones de depuración lo notifican como un error. Donde la API de una plataforma puede exigir acciones, la especificación dice que lo hace: el constructor
actions:de SwiftUI, el parámetroactionsde Compose, la listaactionsde Flutter y la propiedadactionsde React Native. De las cuatro, hoy solo es obligatoria la de SwiftUI; las demás siguen aceptando un diálogo sin acciones hasta que se pongan al día. Las compilaciones de depuración también notifican un diálogo cuyas acciones están todas deshabilitadas mientrasdismissiblees false, porque eso no deja ninguna salida.
Un formulario con Cancelar junto a Guardar, y el ancho small
Un diálogo no dibuja ningún botón de cerrar en su encabezado. Pide una decisión, y su salida sin decidir es su acción de descarte y, mientras dismissible es true, Escape, la acción de retroceso de la plataforma y una pulsación en el velo.
- Un formulario o una elección. Pon una acción de descarte, como Cancelar, junto a la de confirmar, como en el ejemplo.
- Algo para leer. Da al diálogo una acción con la etiqueta Cerrar, o Cancelar si hay algo que cancelar, y márcala
dismissive. Si el contenido solo necesita una salida y ninguna respuesta, muéstralo en una hoja lateral, que tiene botón de cerrar, o en una hoja inferior, que la persona puede cerrar deslizando. - Un diálogo largo. Mientras su contenido se desplaza, las acciones se quedan en su sitio al pie, así que la salida sigue a la vista.
- En la web. Un diálogo al que se le da
closableoclose-labelno dibuja nada por ello, y las compilaciones de depuración avisan de que un diálogo no tiene botón de cerrar.
De forma predeterminada (width content), el diálogo es tan ancho como su parte más ancha, entre 280 y 560. Su ancho sigue entonces a su texto, lo que conviene a una confirmación. Da un valor a width para fijar el ancho, de modo que un formulario lo conserve aunque cambien sus campos y sus mensajes:
width |
Ancho | Para |
|---|---|---|
content |
su parte más ancha, de 280 a 560 | una confirmación o una pregunta breve (el predeterminado) |
small |
400 | uno o dos campos |
medium |
560 | un formulario, una lista de opciones |
large |
800 | contenido que necesita espacio, como una tabla o una vista previa |
Un ancho fijo nunca supera la ventana menos 16 a cada lado, así que un diálogo large en una ventana de 640 mide 608. En una ventana compacta (más estrecha que 600), todos los anchos se colocan como el predeterminado: una hoja abajo, tan ancha como la ventana hasta 640. Un cambio de width con el diálogo abierto se dibuja al instante. Un producto cambia los tres anchos de todos sus diálogos en su capa (width.small, width.medium, width.large).
Cuando la ventana mide menos de 600 de ancho, el diálogo toma el aspecto de la hoja inferior en lugar de situarse en el centro. Es la regla de Tanrei para todos los diálogos y todos los popovers en una ventana compacta. El diálogo se apoya en el borde inferior de la ventana (o sobre el teclado en pantalla), tan ancho como la ventana hasta comp.bottom-sheet.container.max-width (640), con el color y la elevación de la hoja inferior, sus esquinas superiores redondeadas y las inferiores rectas. Su color llega por debajo del margen inferior del sistema, y sus partes conservan el relleno y los estilos de texto del propio diálogo. Solo cuenta el ancho, de modo que un teléfono en horizontal no es compacto y su diálogo aparece centrado.
Ahí parece una hoja inferior, pero sigue siendo un diálogo. Tiene una sola altura y sus salidas son sus acciones, Escape, la acción de retroceso y el velo, así que no tiene tirador de arrastre, no sigue un arrastre y no se cierra al deslizar. Su rol, su foco, lo que deja inerte, su velo y su descarte son los de cualquier diálogo. Cuando la ventana cruza los 600 mientras el diálogo está abierto, pasa a la otra posición de inmediato, sin movimiento, y el foco se queda donde está.
Un sistema que mantiene los diálogos centrados en todas partes define el ancho compacto como 0. Los preajustes m3-like y cupertino lo hacen, ya que los dos originales centran sus diálogos. También conservan las acciones de texto de sus originales. Para mantener un diálogo centrado en cualquier ventana, pon su compactPresentation en own (compact-presentation="own" en la web).
Un diálogo es una superposición modal. Lleva el foco a su interior, lo mantiene dentro y lo devuelve cuando se cierra.
- Apertura. Cuando
openpasa a ser true, el diálogo se presenta por encima de todo lo que hay en su ventana, centrado (o en la parte inferior de una ventana compacta), sobre un velo que atenúa toda la ventana. Todo lo que queda fuera se vuelve inerte: no puede recibir el foco ni pulsarse, y la tecnología de apoyo no puede alcanzarlo. El contenido que hay detrás deja de desplazarse. - Entrada del foco. El foco pasa al elemento que marcaste, o al propio diálogo, de modo que la tecnología de apoyo lee primero el titular y el siguiente Tab llega al primer elemento que puede recibir el foco. El foco se mueve cuando el diálogo se abre, no cuando termina su movimiento.
- Retención del foco. 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 no sale del diálogo.
- Descarte. Mientras
dismissiblees true, Escape, 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) y una pulsación en el velo emitendismissuna vez, y tú cierras el diálogo. En la web el diálogo se cierra solo salvo que cancelescancel, como hace un<dialog>nativo. Mientrasdismissiblees false no hacen nada, y solo una acción cierra el diálogo. Un diálogo situado en la parte inferior de una ventana compacta no se cierra al deslizar.dismisslleva el motivo:escape,backoscrim. - Devolución del foco. Cuando se cierra, el foco vuelve al elemento que lo tenía cuando se abrió el diálogo. El contenido que hay detrás se restaura tal como estaba.
- Apilamiento. Un diálogo abierto desde otro diálogo se apila encima de él con su propio velo y vuelve inerte también al primero. Escape descarta solo el que está más arriba, y al cerrarlo el foco vuelve al elemento del primer diálogo que lo tenía.
- Anuncio. El diálogo se anuncia como un diálogo, con su titular como nombre y su texto de apoyo como descripción. Con
alertes un diálogo de alerta, y la tecnología de apoyo lee el titular y el texto de apoyo en cuanto se abre. - Movimiento. El velo aparece con un fundido. Un contenedor centrado aparece con un fundido mientras crece desde un tamaño algo menor que el suyo. En una ventana compacta, el contenedor sube desde debajo del borde inferior con el movimiento de la hoja inferior, sin crecer, y vuelve a bajar al cerrarse. Donde el movimiento está reducido, se funde sin crecer ni moverse; en Android, cuyo ajuste elimina todas las animaciones, aparece de inmediato.
Cómo lo presenta cada plataforma: un <dialog> nativo abierto con showModal() en la web, .fullScreenCover con fondo transparente en SwiftUI para iOS (en macOS, que no tiene cubierta a pantalla completa, una superposición sobre la vista a la que se aplica el modificador), una ventana Dialog en Compose, una ruta modal en Flutter y un Modal transparente en React Native. Cada una desactiva su propia atenuación y su propia transición, de modo que todas las plataformas dibujan el mismo velo y el mismo movimiento a partir de los tokens.
Un snackbar que todavía no se ha mostrado espera mientras hay un diálogo de Tanrei abierto, y uno que ya se está mostrando pone en pausa su tiempo de espera. Un tooltip dentro de un diálogo se dibuja por encima de él.
- Escribe el titular como la propia pregunta o afirmación: «¿Descartar el borrador?», «¿Eliminar el proyecto?». Evita «¿Seguro?».
- Da a las acciones el nombre de lo que hacen: «Descartar», «Eliminar», «Cerrar sesión». Evita «Sí» y «No».
- Marca cada acción como
confirmingodismissivey deja que el diálogo las coloque. - Da una respuesta a cada diálogo. Un diálogo que solo informa es un snackbar o una línea en la página.
- Da a un diálogo que muestra algo para leer una acción con la etiqueta Cerrar, o Cancelar, o muestra ese contenido en una hoja lateral.
- En una confirmación destructiva, marca «Cancelar» para el foco inicial. En un formulario, marca el primer campo. En un diálogo cuyo texto debe leerse primero, no marques nada.
- Define
alertsolo para interrupciones urgentes: una confirmación destructiva, una conexión perdida.
- No abras un diálogo que la persona no ha pedido, salvo que la tarea de verdad no pueda continuar.
- No añadas un botón de cerrar propio al encabezado ni al contenido: un diálogo no lo tiene, y su salida es una acción de descarte.
- No pongas
dismissibleen false salvo que la persona deba elegir una de las acciones. Quienes usan un pulsador o un lector de pantalla dependen de Escape y de la acción de retroceso. Un diálogo que no se puede descartar sigue necesitando una salida visible, como «Cancelar». - No pongas más que unos pocos datos en un diálogo. Lleva un formulario largo a una página.
- No ordenes las acciones a mano para poner la de confirmación en último lugar, ni definas una variante en cada botón para obtener tonal: el diálogo hace ambas cosas.
- No cambies el estilo de los botones más allá de su
variant, ni añadas controles al marco del diálogo. - No trates el diálogo situado en la parte inferior de una ventana compacta como una hoja inferior. Usa una hoja inferior cuando la persona deba poder apartarla deslizando.
- Rol
dialog- Etiquetado
- The headline is the accessible name and the supporting text the description. The role is dialog, or alertdialog while alert is true (platforms without that distinction read the headline and supporting text on opening). Everything outside the dialog is hidden from assistive technology while it is open. Each action is named by its own label; confirming and dismissive are not announced, and the actions are read in the order they are placed (BGR-15).
- 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 | Move focus through the dialog's focusable elements, wrapping from the last to the first and back; focus never leaves the dialog. |
| Escape | Dismisses the dialog (emits dismiss with the reason escape) while dismissible is true; with nested dialogs, only the topmost. |
| Enter / Space | Activate the focused button (an action: its click). The dialog adds no default action to Enter. |
Notas
- The dialog has no target of its own; its actions are Tanrei Buttons, which meet the minimum target themselves (BTN-5). comp.dialog.actions.gap keeps each action's whole target in a column in the default system.
- Initial focus: mark the least destructive action (Cancel) in a destructive confirmation, the first field in a form, and nothing in a dialog whose text must be read first. Without a mark, focus goes to the container (DLG-2).
- Every dialog offers an action (DLG-19). Use dismissible false sparingly: a person using a switch or a screen reader relies on Escape and back. A dialog that must not be dismissed still needs a visible way out, a dismissive action.
- A dialog has no close button (DLG-28). Its visible way out is a dismissive action, which a person meets in the reading order of the actions; Escape, back and the scrim dismiss it as well while it is dismissible.
- Placement is by task, not by code order: in a row the dismissive action is at the start and the confirming one at the end; in a column the confirming one is on top. Focus and screen readers follow the placed order, so a sighted and a screen-reader user meet the actions in the same order.
- In a compact window the dialog takes the bottom sheet's appearance (DLG-17), as every dialog and every popover of Tanrei does, and is still a modal dialog: the same role, focus, inertness and dismissal, and no drag. A person who expects a bottom sheet's swipe to close it uses Escape, back or the scrim, or a dismissive action. compactPresentation own keeps one dialog centred in every window.
- Text size: at 200% the dialog grows up to the window; its actions stack when a label no longer fits its share (DLG-10), its supporting text and content scroll (DLG-11), and on a very short window everything scrolls together (DLG-12).
- Web: a native `<dialog>` opened with `showModal()` inside the shadow root, so the rest of the page is inert and the dialog is in the top layer. `::backdrop` draws the scrim. The `autofocus` attribute on a slotted element marks initial focus. `form method="dialog"` closes it natively, and the native `close` event follows every close.
- Where a platform presents the dialog in a window of its own (Compose's `Dialog`, React Native's `Modal`), the platform's own window dim is switched off and Tanrei draws the scrim, so every platform draws the same color.
Generado a partir de specs/dialog.spec.json, tokens/sys/measure.tokens.json
- El diálogo se expone como un diálogo modal, o como un diálogo de alerta mientras
alertes true, con su titular como nombre y su texto de apoyo como descripción. Conalert, el titular y el texto de apoyo se leen en cuanto se abre. - Todo lo que queda fuera es inerte y está oculto para la tecnología de apoyo mientras está abierto.
- Al abrirse, el foco pasa al elemento que marcaste, o al propio diálogo. Tab y Mayús+Tab dan la vuelta dentro de él. Al cerrarse, el foco vuelve al elemento que lo tenía cuando se abrió.
- Escape, la acción de retroceso de la plataforma y una pulsación en el velo emiten
dismissmientrasdismissiblees true. Con diálogos apilados, solo se descarta el que está más arriba. - Enter y Espacio activan el botón que tiene el foco. El diálogo no añade ninguna acción predeterminada a Enter.
- Las acciones son botones de Tanrei, que cumplen por sí mismos el objetivo mínimo. Los 16 que hay entre las acciones apiladas mantienen entero cada objetivo.
- El foco y los lectores de pantalla encuentran las acciones en el orden de colocación, el mismo que ve una persona vidente.
- En la parte inferior de una ventana compacta, el diálogo conserva el mismo rol, el mismo foco y el mismo descarte, y deja inerte lo mismo. Quien espere el deslizamiento de una hoja inferior lo cierra con Escape, con la acción de retroceso, con el velo o con una acción de descarte.
- Con el texto al 200%, el diálogo crece hasta ocupar la ventana, sus acciones se apilan cuando una etiqueta ya no cabe en su parte, el texto de apoyo y el contenido se desplazan y, en una ventana muy baja, todo se desplaza junto, de modo que nada queda cortado ni inalcanzable.
- Un diálogo sin titular o sin ninguna acción se abre igualmente, y las compilaciones de depuración notifican el nombre que falta o la respuesta que falta. También notifican un diálogo cuyas acciones están todas deshabilitadas mientras
dismissiblees false.
Carencias conocidas por plataforma:
- SwiftUI, Compose, Flutter y React Native:
dividerVisibilityespera a que esas plataformas se pongan al día. Sus divisores aparecen solo mientras algo queda desplazado fuera de la vista más allá de ellos, como hacescroll. - SwiftUI, Compose, Flutter y React Native:
widthy el motivo que llevadismissesperan a que esas plataformas se pongan al día. - Web: Firefox no puede mantener en la capa superior un diálogo que se está cerrando mientras se desvanece, así que ahí el diálogo desaparece de inmediato al cerrarse, sin el fundido de salida.
- Web: todavía no se ha comprobado en un dispositivo con teclado en pantalla que el diálogo se mantenga apartado de él.
- SwiftUI no tiene rol de diálogo ni de diálogo de alerta. El contenedor lleva el rasgo modal, tiene el titular como nombre y el texto de apoyo como indicación, y el titular lleva el rasgo de encabezado. Con
alert, VoiceOver lee el titular y el texto de apoyo cuando se abre el diálogo. - SwiftUI en macOS no tiene cubierta a pantalla completa: el diálogo cubre solo la vista a la que se aplica el modificador, así que aplícalo a la raíz de la ventana. Esa vista queda deshabilitada mientras el diálogo está abierto, y sus controles dibujan su estado deshabilitado bajo el velo.
- SwiftUI no puede llevar el foco del teclado al propio diálogo. Sin marca, VoiceOver empieza en el titular. Al cerrarse, iOS devuelve el foco de VoiceOver al elemento que lo presentó, pero el foco del teclado no se devuelve ni en iOS ni en macOS.
- Compose no tiene rol de diálogo de alerta. La ventana es un diálogo cuyo nombre es el titular, y un diálogo
alertanuncia su titular y su texto de apoyo al abrirse, mediante una API de Android que Android 16 declara obsoleta. - Compose: la ventana de diálogo real, la vuelta de Tab, el regreso del foco a quien lo abrió, el gesto de retroceso y TalkBack solo se han probado en Robolectric, todavía no en un dispositivo.
- Flutter: cuando ni siquiera el titular y las acciones dejan espacio y el diálogo entero se desplaza junto, ese cambio se decide después de un cálculo de disposición y se ve un fotograma más tarde. Flutter no tiene una propiedad de descripción, así que el texto de apoyo es la indicación del contenedor. El velo no tiene etiqueta accesible salvo que pases
barrierLabel. - React Native no puede saber qué elemento tenía el foco, así que al cerrarse no devuelve el foco al elemento que abrió el diálogo: el foco va adonde iOS y Android lo ponen cuando se cierra una ventana. Sin
initialFocusRef, solo se mueve el lector de pantalla (al titular); el foco de un teclado físico se queda donde la plataforma lo pone en la ventana nueva. - React Native: que Tab dé la vuelta dentro del diálogo depende de la plataforma. Eso, el foco del lector de pantalla, la acción de retroceso, el gesto de escape de iOS y los diálogos anidados todavía no se han comprobado en un dispositivo.
- React Native: el diálogo se mantiene dentro del área segura solo cuando pasas los márgenes interiores de la ventana a
TanreiProvider.
- Da un titular a cada diálogo. Es el nombre del diálogo.
- Da a cada diálogo al menos una acción y marca cada una como
confirmingodismissive. En las plataformas que todavía ofrecen la revisión anterior, pon tú la acción de confirmación en último lugar. - Cierra tú el diálogo: desde el controlador de cada acción y en respuesta a
dismiss(onRequestClose,onDismissRequest, el binding o la ruta, según la plataforma). - Marca el foco inicial donde ayude: «Cancelar» en una confirmación destructiva, el primer campo en un formulario.
- Da a cada diálogo una salida visible, una acción de descarte como «Cancelar», incluso cuando
dismissiblesea false. - En React Native, pasa
initialFocusRef. Después de que el diálogo se cierre, devuelve tú el foco a quien lo abrió si la plataforma no lo hace. - En SwiftUI para macOS, aplica
.tanreiDialoga la vista raíz de la ventana, para que el velo cubra toda la ventana. - En la web, si tu contenido mantiene una fila fija al pie, define
scroll-padding-block-enden::part(scroll)con la altura de esa fila, para que el elemento con el foco nunca quede oculto debajo de ella.