dialog: la diferencia real entre show() y showModal()
Qué hace cada método, qué te da gratis el modo modal, cómo se cierra un diálogo y cómo se recoge su resultado sin escribir un gestor de estado.
dialog tiene dos métodos de apertura que parecen variantes del mismo y no lo son: show() abre un panel normal, showModal() abre un modal de verdad, con capa superior, inercia del resto del documento, gestión del foco y tecla de escape. Elegir mal es la causa de la mitad de los diálogos accesibles a medias que hay por ahí.
- Enumerar las cinco diferencias entre
show()yshowModal(). - Cerrar un diálogo y recoger su resultado sin escribir estado.
- Gestionar el foco inicial y el foco de retorno.
- Conocer el estado real del atributo
closedby.
Las cinco diferencias
show() |
showModal() |
|
|---|---|---|
| capa superior | no | sí |
::backdrop |
no se genera | sí |
| resto del documento | interactivo | inerte |
| tecla de escape | no cierra | cierra, tras el evento cancel |
| foco | no se mueve solo | se mueve dentro y queda atrapado |
show() deja el diálogo como un elemento normal del documento con position: absolute según la hoja de usuario. Está visible, es interactivo, y todo lo demás también. Es un panel, no un modal. Su uso legítimo es un cuadro de herramientas que acompaña a la edición, no una confirmación.
showModal() hace las cinco cosas de la columna derecha como una sola operación atómica. Lo importante no es que sean cinco funcionalidades: es que no se pueden desincronizar. No hay ningún estado en el que el diálogo esté visualmente encima pero el fondo siga siendo tabulable.
En los dos casos, el atributo open refleja si el diálogo está abierto. Y en los dos casos, escribir open a mano en el HTML no es equivalente a llamar al método: un dialog con open en el marcado se muestra como si hubieras llamado a show(), sin capa superior ni inercia. Es un error frecuente y silencioso.
<!-- esto NO es un modal -->
<dialog open>Confirmar</dialog>
Cerrar y recoger el resultado
Un form con method="dialog" dentro de un dialog tiene un comportamiento especial: al enviarse, en lugar de navegar, cierra el diálogo y guarda el value del botón que lo envió en la propiedad returnValue.
<dialog id="confirmar">
<form method="dialog">
<p>¿Eliminar los tres elementos seleccionados?</p>
<button value="cancelar">Cancelar</button>
<button value="eliminar" autofocus>Eliminar</button>
</form>
</dialog>
const dlg = document.querySelector('#confirmar');
dlg.addEventListener('close', () => {
if (dlg.returnValue === 'eliminar') eliminarSeleccion();
});
document.querySelector('#borrar').addEventListener('click', () => {
dlg.showModal();
});
Sin una sola línea de gestión de estado: el marcado declara los resultados posibles, el navegador guarda cuál se eligió, y el evento close te avisa. Si el usuario cierra con la tecla de escape, returnValue queda como estaba —cadena vacía la primera vez—, lo que hace que el if de arriba trate el escape como cancelación de forma natural.
También puedes cerrarlo desde JavaScript con dlg.close('eliminar'), pasando el valor de retorno como argumento.
Dos eventos que conviene distinguir:
cancel se dispara cuando el usuario pide cerrar con la tecla de escape, antes de que se cierre. Es cancelable: llamar a preventDefault() impide el cierre, que es lo que necesitas si hay cambios sin guardar.
close se dispara siempre que el diálogo se cierra, por el motivo que sea. No es cancelable.
dlg.addEventListener('cancel', (e) => {
if (hayCambiosSinGuardar()) e.preventDefault();
});
El foco
showModal() mueve el foco al primer elemento enfocable del diálogo, o al propio diálogo si no hay ninguno, y lo mantiene dentro mientras está abierto. Al cerrarse, lo devuelve al elemento que lo tenía antes de abrir.
El control fino se hace con autofocus, que en un diálogo modal sí se respeta cada vez que se abre, no solo en la carga de la página.
<button value="eliminar" autofocus>Eliminar</button>
La regla de diseño que conviene seguir: enfoca la acción menos destructiva por defecto si el diálogo es una confirmación peligrosa, y la más probable si es un formulario normal. Y no enfoques un campo de texto en móvil salvo que el teclado deba abrirse de inmediato.
Un detalle que a menudo se pasa: el diálogo debe tener un nombre accesible. aria-labelledby apuntando al título es la forma habitual.
<dialog id="confirmar" aria-labelledby="t-confirmar">
<h2 id="t-confirmar">Eliminar elementos</h2>
...
</dialog>
flowchart TB
A[Necesitas mostrar algo por encima] --> B{El usuario debe poder seguir usando el resto}
B -->|Si| C{Debe cerrarse al pulsar fuera}
C -->|Si| D[Elemento con popover auto]
C -->|No| E[dialog con show o popover manual]
B -->|No, es una decision bloqueante| F[dialog con showModal]
F --> G[Capa superior mas backdrop mas inercia mas foco mas escape]
G --> H[Recoge el resultado con form method dialog]
style A fill:#89b4fa,color:#11111b
style D fill:#a6e3a1,color:#11111b
style F fill:#a6e3a1,color:#11111b
style G fill:#cba6f7,color:#11111b
style H fill:#a6e3a1,color:#11111bCerrar al pulsar fuera, y el estado de closedby
Un modal no se cierra al pulsar fuera por defecto, y muchos diseños lo piden. La técnica clásica aprovecha que el ::backdrop no es un elemento del DOM: un clic sobre él tiene como target el propio dialog, mientras que un clic sobre el contenido tiene como target algo de dentro.
dlg.addEventListener('click', (e) => {
if (e.target === dlg) dlg.close();
});
Funciona, pero tiene un fallo conocido: si el usuario empieza a arrastrar dentro del diálogo y suelta fuera, el clic se registra sobre el diálogo y se cierra sin querer. La corrección es comprobar la geometría en mousedown, o usar el atributo estándar.
Ese atributo estándar es closedby, con tres valores: any —cierra al pulsar fuera y con escape—, closerequest —solo con escape, que es el comportamiento por defecto de un modal— y none —no se cierra por ninguna de las dos vías—.
<dialog id="confirmar" closedby="any">...</dialog>
Y aquí va el dato que hay que tener presente: closedby no está en los tres motores. Chrome lo tiene desde la 134 y Firefox desde la 141, pero a agosto de 2026 Safari solo lo ha implementado en versiones preliminares. No es Baseline.
La estrategia es la habitual: usar el atributo, y añadir el manejador de clic como respaldo detectando si el atributo se soporta.
if (!('closedBy' in HTMLDialogElement.prototype)) {
dlg.addEventListener('click', (e) => { if (e.target === dlg) dlg.close(); });
}
La diferencia entre los dos métodos que casi nadie tiene interiorizada, y que produce un fallo especialmente difícil de atribuir, es esta: show() no promociona el elemento a la capa superior. Un dialog abierto con show() es un elemento del documento como cualquier otro, con position: absolute según la hoja de usuario, y por tanto está sujeto a todo lo que puede estropear un elemento posicionado: si vive dentro de un ancestro con transform, su bloque contenedor es ese ancestro y no el viewport; si vive dentro de un contexto de apilamiento con z-index bajo, queda por debajo de lo que haya fuera; si vive dentro de un overflow: hidden, se recorta. El síntoma en producción es desconcertante porque el mismo componente funciona en unas páginas y no en otras, y el código del diálogo es idéntico. La confusión se agrava porque el modelo mental de la mayoría es “dialog es el elemento de la capa superior”, cuando en realidad es “showModal() es el método de la capa superior”. La regla operativa: si el elemento tiene que estar por encima de todo pase lo que pase, o usas showModal() o usas popover; show() te da un panel, con todas las fragilidades de un panel.
- Abre el mismo diálogo con
show()y conshowModal()y comprueba las cinco diferencias de la tabla. - Mete el diálogo dentro de un ancestro con
transformy compara el resultado con los dos métodos. - Recoge el resultado con
form method="dialog"y verifica qué valereturnValueal cerrar con escape. - Impide el cierre con escape cuando haya cambios sin guardar, usando el evento
cancel. - Prueba
closedby="any"en tu navegador y comprueba si la propiedadclosedByexiste en el prototipo.