Descripción general
Usa el elemento nativo <dialog> para los diálogos modales y no modales. Proporciona la semántica de accesibilidad, el manejo del enfoque, la renderización de la capa superior y el manejo de Esc que las superposiciones personalizadas deben volver a compilar de forma manual.
Elige el tipo de superposición
Elige la primera regla que coincida. Si no lo sabes con certeza, usa la regla 1.
Regla 1: La superposición interrumpe al usuario y bloquea la página (predeterminada)
Úsala para confirmaciones, formularios, accesos, acciones destructivas y cualquier solicitud "modal", "de diálogo", "lightbox" o "emergente" que no indique lo contrario.
Usa <dialog> abierto como un modal, de forma declarativa con command="show-modal" (consulta Control declarativo de diálogos y ventanas emergentes) o desde una secuencia de comandos con showModal().
<button type="button" command="show-modal" commandfor="confirm">Delete</button>
<!-- MANDATORY: no role="dialog" or aria-modal; the native modal already provides them -->
<dialog id="confirm" aria-labelledby="confirm-title">
<h2 id="confirm-title">Delete this item?</h2>
<form method="dialog">
<button value="cancel" autofocus>Cancel</button>
<button value="delete">Delete</button>
</form>
</dialog>
El navegador proporciona lo siguiente: página inerte, contención de enfoque, capa superior, ::backdrop, Esc y descarte de gestos de retroceso, y retorno del enfoque al invocador.
- NO agregues
role="dialog",aria-modal, una trampa de enfoque, un botón de activacióninertni un controladorEsc. - NO lo abras con
show()ni con el atributoopen. - SÍ detén el desplazamiento de la página detrás del anuncio con el CSS que se muestra a continuación. Consulta Cómo evitar que se desplace la página que se encuentra debajo de un diálogo modal para obtener más detalles.
- NO bloquees el desplazamiento con
overflow: hiddenoposition: fixedenhtmlobody.
/* MANDATORY (both rules): without them, scrolling over the backdrop or inside the dialog scrolls the page behind it */
dialog:modal {
overscroll-behavior: contain; /* stops scroll gestures in the dialog chaining to the page */
}
dialog::backdrop {
overflow: hidden; /* ::backdrop is not a scroll container by default, so it would ignore overscroll-behavior */
overscroll-behavior: contain;
}
No se usa para menús ni cuadros de información (consulta la regla 3).
Regla 2: El usuario sigue trabajando en la página mientras el panel está abierto
Se usa para barras de búsqueda, inspectores, paletas y paneles de redacción que, semánticamente, siguen siendo diálogos.
Usa <dialog popover> para un panel de capa superior con descarte de ventana emergente (auto descarta de forma ligera, manual no). Usa dialog.show() solo cuando el panel no deba estar en la capa superior.
<button type="button" popovertarget="find">Find</button>
<dialog id="find" popover="manual" aria-labelledby="find-title">
<h2 id="find-title">Find</h2>
<input type="search" aria-label="Search">
<button type="button" popovertarget="find" popovertargetaction="hide">Close</button>
</dialog>
La página sigue siendo interactiva y no se vuelve inerte.
- SÍ incluye un botón de cierre visible.
- NO uses
showModal(), que bloquea la página.
No se deben usar para bloquear confirmaciones (consulta la regla 1).
Regla 3: La superposición no es un diálogo
Se usa para menús, menús desplegables, cuadros de información, tarjetas emergentes y mensajes de aviso.
Usa un elemento semántico con el atributo popover, no <dialog>.
<button type="button" popovertarget="menu">Actions</button>
<ul id="menu" popover>
<li><a href="/rename">Rename</a></li>
</ul>
Elige cómo se puede descartar
El valor predeterminado depende de cómo se abra el diálogo. Si closedby no está presente o no es válido, showModal() se comporta como closerequest y cualquier otra forma de apertura se comporta como none.
- Solo gestos de Esc y Atrás (modal predeterminado): No configures nada o
closedby="closerequest". - También haz clic fuera para cerrar: usa
closedby="any". - Solo el desarrollador lo cierra (por ejemplo, un formulario con cambios sin guardar): Usa
closedby="none"y agrega una ruta de confirmación. Consulta la sección Cierre que aparece más abajo.
Lineamientos
Nombre de accesibilidad y enfoque inicial
- SÍ asigna un nombre a cada diálogo con
aria-labelledbyque apunte a su encabezado oaria-labelcuando no haya un título visible. - SÍ coloca
autofocusen el control principal o en el primer campo. - SÍ debes colocar
autofocusen la acción segura (como Cancelar o Conservar) en una confirmación destructiva. - NO escribas código para devolver el foco al cerrar. El navegador lo devuelve al invocador.
Opening
- SÍ abre de forma declarativa con
command="show-modal"ycommandforen un<button>. Consulta Control declarativo de diálogos y ventanas emergentes. - SÍ uses
dialog.showModal()cuando abras desde una secuencia de comandos.
Closing
- SÍ cierres con botones
<form method="dialog">ocommand="close". - SÍ elige
closedbyde forma deliberada:any(descarte ligero yEsc),closerequest(soloEsc, el valor predeterminado modal) onone(solo cierre controlado por el desarrollador). - SÍ usa
command="request-close"orequestClose()para los botones Cancelar que deben respetar la lógica de cambios no guardados. Activancancelprimero y, luego, ignoranclosedby. - SÍ trata
closedbycomo una mejora progresiva.Escy el gesto de retroceso en dispositivos móviles ya cierran los modales sin él.
Cómo conectarse a close
- SÍ lee
dialog.returnValueen un objeto de escuchaclosepara saber qué botón se usó y, luego, restablecerlo condialog.returnValue = ''.Escy otras solicitudes de cierre no establecen ningún valor, y los navegadores difieren en cuanto a si borran el anterior, por lo que, sin el restablecimiento, se puede filtrar un valor obsoleto. Trata''como "descartado sin elegir". - SÍ uses
cancelpara interceptarEscyrequest-close, pero no dependas depreventDefault()para presionarEscde forma repetida sin la activación del usuario. - SÍ uses
closedby="none"con un paso de confirmación explícito para los formularios modificados. - SÍ indícale al usuario por qué el diálogo permaneció abierto cuando se impidió
cancel: muestra un mensaje visible y haz referencia a él conaria-describedbydesde el control que recibe el enfoque (como Descartar), de modo que un lector de pantalla lo lea cuando se mueva el enfoque. Sin él, cuando el enfoque se mueve a un botón nuevo, solo se anuncia el nombre de ese botón. - NO permitas que la confirmación mueva los controles debajo del puntero. Reserva el espacio del mensaje (ocúltalo con
visibility, no condisplayni conhidden) y agrega el botón destructivo donde los botones existentes no se muevan (por ejemplo, primero en una fila alineada a la derecha), de modo que un segundo clic en el mismo lugar no pueda descartar los cambios del usuario.
Diálogos no modales
- SÍ usa
<dialog popover>(autoomanual) para un panel de capa superior ydialog.show()cuando no quieras la capa superior. - SÍ incluye un botón de cierre visible en cada diálogo no modal.
Fondo y estilo
- SÍ aplica diseño a
::backdropy usadialog:modalpara segmentar los estilos solo modales. - SÍ dejes la animación en Animate Elements To and From Top Layer, el arrastre en Build a Draggable Element y el cambio de elemento superior en Persistent Top Layer UI.
- NO configures
displayendialogde forma incondicional. Anula el estado cerrado. Defínelo condialog[open].
Accesibilidad
- NO agregues
role="dialog",aria-modal, una trampa de enfoque manual ni un botón de activación deinerta un modal nativo. - SÍ usa un
<button>nativo con un nombre de accesibilidad para los controles de cierre. - SÍ usa un nivel de encabezado coherente para los títulos de los diálogos.
Estrategias de resguardo
closedby (dialog-closedby)
Sin closedby, un modal se sigue cerrando con Esc y el gesto de retroceso; solo se pierde el clic fuera del modal, por lo que se trata de una degradación elegante. closedby="none" se ignora de la misma manera: Esc activa cancel y, luego, cierra el diálogo. Mantén la protección de cambios no guardados en un controlador cancel, no solo en closedby="none", para que también proteja donde falta closedby. Si tu destino de Baseline requiere un cierre rápido, agrega lo siguiente donde falta closedby:
if (!('closedBy' in HTMLDialogElement.prototype)) {
// Test the pointer position, not event.target: the <dialog> is also the target for its own
// padding and the gaps between its children, so a target check would close on those clicks
const outside = (e, dialog) => {
const r = dialog.getBoundingClientRect();
return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
};
for (const dialog of document.querySelectorAll('dialog[closedby="any"]')) {
let pressedOutside = false;
dialog.addEventListener('pointerdown', (e) => { pressedOutside = outside(e, dialog); });
// Require the press and the release outside, so a text-selection drag that ends on the backdrop does not dismiss
dialog.addEventListener('pointerup', (e) => { if (pressedOutside && outside(e, dialog)) dialog.close(); });
}
}
overscroll-behavior
Trata la contención de desplazamiento como una mejora progresiva. No uses la detección de funciones: @supports informa que la propiedad es compatible, incluso cuando los contenedores que no se pueden desplazar la ignoran. Cuando no se aplica, el diálogo sigue conteniendo el desplazamiento cada vez que se desborda su propio contenido, y showModal() sigue haciendo que la página esté inerte. No recurras a overflow: hidden ni position: fixed en html ni body, ya que esto quita la barra de desplazamiento raíz y cambia el diseño.
Comandos del invocador (command/commandfor)
Si tu objetivo de Baseline no incluye comandos de invocador, mantén el mismo lenguaje de marcado y carga el polyfill de forma condicional (la misma experiencia, como mejora progresiva). Detecta en la interfaz y nunca la cargues de forma incondicional:
// MANDATORY: feature detect on HTMLButtonElement.prototype and load only when native support is missing
if (!('commandForElement' in HTMLButtonElement.prototype)) {
import('invokers-polyfill'); // npm install invokers-polyfill
}
Solo las reglas 2 y 3 usan Popover.
Resguardos y compatibilidad de navegadores para Popover
La API de Popover es, en su mayor parte, una mejora progresiva, pero sus comportamientos definitorios (promoción de la capa superior, cierre rápido y la invocación de popovertarget) no tienen un equivalente solo de CSS. Los navegadores más antiguos necesitan un polyfill o un resguardo manual si prefieres no enviar uno.
Polyfill: Para admitir el atributo popover en navegadores más antiguos, carga @oddbird/popover-polyfill de forma condicional. OBLIGATORIO: Detecta la función verificando la propiedad popover en HTMLElement.prototype y carga el polyfill solo cuando no haya compatibilidad nativa. NO lo cargues de forma incondicional.
Con un empaquetador o un mapa de importación:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
Sin un empaquetador, importa desde una CDN dentro de un <script type="module">:
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
Advertencia de diseño: El polyfill no puede definir la pseudoclase :popover-open real, por lo que aplica una clase .\:popover-open en su lugar. OBLIGATORIO: Combina las dos con :is() o :where(); de lo contrario, los navegadores que no tengan :popover-open descartarán toda la regla:
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
Como alternativa, para un resguardo heredado sin un polyfill, usa position: fixed y calcula manualmente las coordenadas a través de getBoundingClientRect() o confía en el posicionamiento predeterminado con inset: auto si eso es aceptable para el caso de uso.