// html · Web Platform Advent #18
El elemento dialog de HTML: modales reales sin ninguna librería
El elemento dialog de HTML te da un modal de verdad (capa superior, página inerte, fondo y gestión de Esc) con solo marcado. Aquí tienes showModal frente a show, form method=dialog, returnValue, el atributo closedby y la costumbre que conviene abandonar.
Todo desarrollador front-end ha construido un modal a mano: un div con posición fija, una guerra de z-index, un detector de clic exterior, un manejador de Esc y algunos malabares con el foco para que el teclado no se pierda por detrás de la superposición. El elemento <dialog> sustituye todo eso con una etiqueta y una llamada a un método. Esto es lo que hace realmente, y los dos o tres detalles que deciden si tu diálogo se comporta como debe.
El diálogo funcional más pequeño
Un diálogo es marcado inerte hasta que lo abres desde JavaScript:
<dialog id="confirm">
<p>¿Eliminar este archivo?</p>
<button autofocus>Cancelar</button>
</dialog>
<button id="open">Eliminar</button> const dialog = document.getElementById("confirm");
document.getElementById("open")
.addEventListener("click", () => dialog.showModal()); Esa única llamada a showModal() hace bastante más que volver visible un elemento.
showModal() frente a show(): la diferencia que importa
Estos dos métodos no son variaciones de una misma idea. Producen cosas realmente distintas.
showModal() abre un diálogo modal. El navegador lo coloca en la capa superior, así que se dibuja por encima de toda la página sin que intervenga ningún z-index. Dibuja un pseudoelemento ::backdrop detrás. Vuelve todo lo demás inerte, lo que significa que el resto del documento no se puede pulsar ni alcanzar con el tabulador. Y establece de forma implícita aria-modal="true".
show() abre un diálogo no modal. La página sigue siendo interactiva, no hay capa superior, no hay fondo, y aria-modal es false.
La inercia es la parte que la gente subestima. Hacer que el fondo no responda al clic es fácil; hacer que no sea accesible con el tabulador es lo que casi siempre fallan los modales hechos a mano, y aquí el navegador lo resuelve por ti.
La costumbre que hay que abandonar: el atributo open
Un <dialog> tiene además un atributo booleano open, y recurrir a él es el error más común con este elemento.
<!-- funciona, pero esto NO es un modal -->
<dialog open>Soy no modal, esperaras lo que esperaras.</dialog> Un diálogo mostrado mediante el atributo open siempre es no modal. Sin capa superior, sin fondo, sin página inerte. MDN recomienda usar show() o showModal() en lugar del atributo, y describe alternar open a mano como una práctica no recomendada. Si tu modal se ve bien pero la página que hay detrás sigue respondiendo a los clics, esta es casi con toda seguridad la razón.
form method="dialog": la función que no tiene equivalente
Esta es la parte de <dialog> que nada más replica, y es la razón por la que un diálogo de confirmación puede no necesitar más JavaScript que el de abrirlo.
<dialog id="confirm">
<form method="dialog">
<p>¿Eliminar este archivo?</p>
<button value="cancel" autofocus>Cancelar</button>
<button value="delete">Eliminar</button>
</form>
</dialog> Cuando un formulario dentro de un diálogo se envía con method="dialog", el diálogo se cierra en lugar de mandar una petición. Los estados de los controles del formulario se guardan pero no se envían, y el returnValue del diálogo toma el valor del botón que se activó.
Así que la respuesta la lees después:
dialog.addEventListener("close", () => {
if (dialog.returnValue === "delete") {
// el usuario ha confirmado
}
}); Dos botones, un atributo, y ya sabes cuál se pulsó. Sin manejadores de clic, sin envoltorio de promesa, sin variable de estado.
La trampa de los campos obligatorios
Hay un comportamiento que sorprende la primera vez y conviene conocer antes de que te cueste una tarde.
Si el diálogo contiene un formulario con campos required, el envío sigue ejecutando la validación. Un botón normal no puede cerrar el diálogo mientras esos campos no sean válidos, lo que significa que tu botón Cancelar deja de funcionar justo cuando el usuario más lo necesita. Las respuestas documentadas son añadir formnovalidate al botón que descarta el diálogo, o llamar a dialog.close() desde tu propio código:
<button value="cancel" formnovalidate>Cancelar</button> Esc y el atributo closedby
El descarte por teclado no es uniforme, y la diferencia sigue la división entre modal y no modal.
Un diálogo abierto con showModal() se puede descartar con Esc de forma predeterminada. Un diálogo no modal no se descarta con Esc por defecto.
El atributo closedby hace esto explícito en lugar de implícito:
<dialog closedby="any"> <!-- Esc, clic fuera, o tu código -->
<dialog closedby="closerequest"> <!-- Esc o tu código -->
<dialog closedby="none"> <!-- solo tu código --> closedby="any" es lo que te da el light-dismiss, ese comportamiento de clic fuera que la gente suele escribir a mano. closedby="none" está pensado para el raro diálogo que no debe descartarse por accidente, y viene con una obligación: tienes que ofrecer una salida visible.
Acertar con la accesibilidad
El elemento te resuelve casi todo esto, pero tres puntos dependen de ti.
Usa autofocus de forma deliberada. Con showModal(), el foco aterriza en el primer elemento enfocable anidado. Muchas veces no es el que quieres. Pon autofocus en el elemento con el que el usuario debe interactuar de inmediato, o en el botón de cierre si no hay nada más urgente.
Ofrece siempre un botón de cierre. La forma más robusta de garantizar que cualquier usuario pueda salir del diálogo es un botón explícito, no solo Esc o un clic fuera.
No pongas tabindex en el propio diálogo. No es interactivo y no recibe el foco. Su contenido sí.
Dar estilo al fondo
La capa atenuada que hay detrás de un modal es un pseudoelemento real al que puedes dar estilo:
dialog::backdrop {
background: rgb(0 0 0 / 0.5);
backdrop-filter: blur(2px);
} Solo existe en los diálogos abiertos con showModal(), lo que es otra manera de comprobar de un vistazo si de verdad estás en modo modal.
¿dialog o popover?
Ambos se dibujan en la capa superior, así que la pregunta surge constantemente. La distinción tiene que ver con la interrupción.
Usa <dialog> con showModal() cuando el usuario deba resolver algo antes de continuar: una confirmación, una elección obligatoria, un formulario que bloquea el flujo. Bloquear la página es precisamente el objetivo.
Usa la Popover API para la interfaz superpuesta y pasajera que no debe interrumpir: menús, tooltips, tarjetas, notificaciones. Ahí la página debe seguir siendo utilizable.
Una prueba útil: si estaría mal que el usuario lo ignorase y siguiera adelante, es un diálogo. Si ignorarlo es una respuesta perfectamente razonable, es un popover.
En resumen
El elemento <dialog> convierte una pieza de interfaz realmente difícil en simple marcado. Ábrelo con showModal() y obtienes la capa superior, un fondo, una página inerte y la gestión de Esc sin escribir nada de eso. Usa form method="dialog" y returnValue y obtienes la respuesta del usuario sin un solo manejador de clic. Evita el atributo open, acuérdate de formnovalidate en tu botón de cancelar, y coloca autofocus tú mismo.
Lo que queda es un diálogo de confirmación en unas quince líneas de HTML que se comporta correctamente para quienes usan teclado y lector de pantalla, que es más de lo que puede decirse de la mayoría de los que ha venido a sustituir.
Preguntas frecuentes
- ¿Cuál es la diferencia entre show() y showModal()?
- showModal() abre un diálogo modal: el navegador lo coloca en la capa superior, dibuja un ::backdrop detrás, vuelve inerte el resto de la página para que nada de fuera se pueda pulsar ni alcanzar con el tabulador, y establece de forma implícita aria-modal="true". show() abre un diálogo no modal: el resto de la página sigue siendo interactivo, no hay capa superior ni fondo, y aria-modal es false. Si quieres un modal de verdad, showModal() es el método adecuado.
- ¿Debería abrir un diálogo con el atributo open?
- No. MDN afirma con claridad que se recomienda usar el método show() o showModal() en lugar del atributo open. Un diálogo mostrado mediante el atributo open siempre es no modal, así que no obtienes nada de la capa superior, el fondo o la inercia que probablemente buscabas. Alternar el atributo a mano se describe explícitamente como una práctica no recomendada.
- ¿Qué hace form method="dialog"?
- Un formulario dentro de un diálogo con method="dialog" cierra el diálogo al enviarse, en lugar de mandar una petición. Los estados de los controles del formulario se guardan pero no se envían, y la propiedad returnValue del diálogo toma el valor del botón que se activó. Eso te dice qué botón pulsó el usuario sin nada de JavaScript.
- ¿La tecla Esc cierra un diálogo?
- En un modal abierto con showModal(), sí: se puede descartar con Esc de forma predeterminada. En un diálogo no modal, no, Esc no lo descarta por defecto. El atributo closedby permite cambiarlo: closerequest admite Esc, any admite además una pulsación fuera del diálogo, y none significa que solo tu propio código puede cerrarlo.
- ¿Por qué mi diálogo no se cierra cuando el formulario tiene un campo obligatorio?
- Porque el formulario sigue ejecutando la validación. Si el diálogo contiene campos required, un botón de envío normal no puede cerrarlo mientras esos campos no sean válidos. Añade formnovalidate al botón destinado a descartar el diálogo, o llama a dialog.close() desde tu propio código.