El atributo popover: auto, manual y el light dismiss
Qué hace el atributo popover, en qué se diferencian sus modos, cómo funciona el cierre suave y la pila de popovers anidados, y la API de JavaScript.
popover es un atributo global de HTML que convierte cualquier elemento en una superposición de la capa superior, con cierre automático al pulsar fuera, gestión de la tecla de escape y una pila que se comporta bien cuando anidas unos dentro de otros. Es el mecanismo que sustituye a la práctica totalidad de los menús, tooltips y paneles flotantes escritos a mano, y su superficie de API es diminuta.
- Declarar un popover y abrirlo sin JavaScript.
- Distinguir los modos
autoymanualy elegir el correcto. - Explicar qué es el light dismiss y cuándo cierra qué.
- Usar la API de JavaScript y sus eventos.
Lo mínimo
<button popovertarget="ajustes">Ajustes</button>
<div id="ajustes" popover>
<h3>Ajustes</h3>
<label><input type="checkbox"> Modo compacto</label>
</div>
Eso es todo. El botón abre y cierra el panel, el panel se pinta por encima de todo, se cierra al pulsar fuera, se cierra con la tecla de escape, y el foco se comporta razonablemente. Cero JavaScript.
El atributo popovertarget va en un button o en un input de tipo botón y apunta al id del popover. Un segundo atributo, popovertargetaction, dice qué hacer: toggle —el valor por defecto—, show o hide.
<button popovertarget="ajustes" popovertargetaction="hide">Cerrar</button>
Ese botón de cerrar puede estar dentro del propio popover, que es el patrón habitual.
Un elemento con popover está oculto por defecto: la hoja de usuario le aplica display: none mientras no se muestre. Cuando se muestra, recibe position: fixed, se centra en el viewport y queda con inset: 0 y margin: auto según los estilos de usuario. Eso último sorprende la primera vez: un popover sin estilos aparece en el centro de la pantalla, no junto a su botón. Colocarlo junto al botón es trabajo de CSS, y es el tema de la última lección del nivel.
El pseudo-selector :popover-open permite estilar el estado abierto.
[popover] { border: 1px solid; border-radius: .5rem; padding: 1rem; }
[popover]:popover-open { box-shadow: 0 8px 24px oklch(0% 0 0 / .2); }
popover está en los tres motores desde abril de 2024: Chrome 114, Safari 17 y Firefox 125.
auto frente a manual
El atributo acepta un valor. popover a secas equivale a popover="auto".
auto es el modo con comportamiento completo:
- Light dismiss: se cierra al pulsar en cualquier sitio fuera de él.
- Se cierra con la tecla de escape.
- Cierra a los demás: al abrirse, cierra cualquier otro popover
autoque no sea un antepasado suyo en la pila.
manual desactiva las tres cosas. No se cierra al pulsar fuera, no responde al escape, y no cierra a nadie ni nadie lo cierra a él. La única forma de cerrarlo es un botón con popovertargetaction="hide" o una llamada a hidePopover().
La elección se decide con una pregunta: ¿pueden coexistir varios a la vez? Un menú desplegable es auto, porque abrir otro debe cerrar el anterior. Un toast de notificación es manual, porque tres notificaciones apiladas son perfectamente razonables y no deben cerrarse porque el usuario haga clic en cualquier sitio.
<div id="notificacion" popover="manual">Cambios guardados</div>
notificacion.showPopover();
setTimeout(() => notificacion.hidePopover(), 4000);
Existe un tercer valor, popover="hint", pensado para tooltips que no deben cerrar al menú desde el que se muestran. Su soporte a agosto de 2026 es parcial: Chrome desde la 133 y Firefox desde la 149 lo implementan con una versión anterior de la especificación y comportamientos inconsistentes, y Safari no lo tiene. No lo uses todavía en producción.
La pila y el anidamiento
Los popovers auto forman una pila. Cuando abres uno, el navegador cierra todos los que no sean antepasados suyos en la relación de anidamiento.
La relación de anidamiento se establece de dos maneras: por contención en el DOM —el popover B está dentro del popover A— o por invocación —el botón que abre B está dentro de A—. La segunda es la que permite tener el submenú en otro sitio del documento y que la pila siga funcionando.
<div id="menu" popover>
<button popovertarget="submenu">Exportar…</button>
</div>
<div id="submenu" popover>
<button>Como PDF</button>
<button>Como CSV</button>
</div>
Al abrir submenu, menu no se cierra, porque el botón que lo invoca está dentro de menu. Pulsar fuera de los dos cierra los dos. Pulsar dentro de menu pero fuera de submenu cierra solo submenu. Es exactamente el comportamiento que uno esperaría de un menú con submenús, y no hay que escribirlo.
flowchart TB
A[Se abre un popover auto] --> B[El navegador mira la pila actual]
B --> C{Los abiertos son antepasados del nuevo}
C -->|Si| D[Se quedan abiertos y el nuevo se apila encima]
C -->|No| E[Se cierran y el nuevo ocupa su sitio]
F[El usuario pulsa fuera] --> G[Se cierran todos hasta el nivel del punto pulsado]
H[El usuario pulsa escape] --> I[Se cierra el de arriba de la pila]
style A fill:#89b4fa,color:#11111b
style D fill:#a6e3a1,color:#11111b
style E fill:#f9e2af,color:#11111b
style G fill:#a6e3a1,color:#11111b
style I fill:#a6e3a1,color:#11111bLa API de JavaScript
Tres métodos y dos eventos.
const p = document.querySelector('#ajustes');
p.showPopover(); // muestra
p.hidePopover(); // oculta
p.togglePopover(); // alterna; acepta un booleano para forzar el estado
togglePopover(true) fuerza mostrar y togglePopover(false) fuerza ocultar, lo que evita la carrera clásica de leer el estado y actuar sobre él.
Los eventos son beforetoggle y toggle, y los dos traen oldState y newState con los valores "open" y "closed".
p.addEventListener('beforetoggle', (e) => {
if (e.newState === 'open') cargarDatosDelPanel();
});
beforetoggle se dispara antes del cambio y es cancelable, lo que permite impedir la apertura o el cierre. toggle se dispara después y no lo es. El uso más rentable de beforetoggle es cargar contenido perezosamente justo antes de mostrar el panel.
Un detalle sobre la accesibilidad que el atributo te da gratis: la relación entre el botón con popovertarget y el popover se expone al árbol de accesibilidad, y el navegador gestiona el estado expandido. No hace falta añadir aria-expanded a mano cuando usas el atributo; sí hace falta si abres el popover solo desde JavaScript sin un invocador declarado.
Todo el mundo ha escrito alguna vez la versión casera del cierre al pulsar fuera: un addEventListener('click', ...) en document que comprueba si el objetivo está dentro del panel y si no, lo cierra. Funciona en la demo y falla en producción por al menos cuatro sitios, y merece la pena saber cuáles porque explican qué te está dando la plataforma. Uno: la fase. Si el botón que abre el panel también dispara ese listener del documento, el panel se cierra en el mismo clic que lo abrió; la corrección casera es una bandera temporal o stopPropagation(), que a su vez rompe cualquier otro listener legítimo del documento. Dos: el foco. El cierre al pulsar fuera es solo la mitad; también hay que cerrar cuando el foco sale por tabulación, y eso es otro listener con su propia lógica de contención que además tiene que distinguir el foco que sale del que va a un descendiente. Tres: la pila. Con dos paneles anidados, decidir cuáles cerrar exige un registro global ordenado y saber quién es antepasado de quién; casi nadie lo implementa y por eso los submenús caseros cierran de más. Cuatro: los iframes y el contenido de fuera del documento. Un clic en un iframe no genera ningún evento en tu documento, así que el panel se queda abierto. El light dismiss nativo se define a nivel del navegador, no del árbol del DOM, y por eso resuelve los cuatro. La conclusión que conviene extraer: cuando la plataforma añade una primitiva que ya sabías escribir, lo que estás comprando no es el caso feliz, son los cuatro casos que tu versión no cubría.
- Crea un popover
autoy comprueba las tres partes del light dismiss: clic fuera, escape y apertura de otro. - Cámbialo a
manualy verifica que ninguna de las tres lo cierra. - Monta un menú con submenú invocado desde dentro y comprueba que abrir el submenú no cierra el menú.
- Usa
beforetogglepara cargar el contenido del panel la primera vez que se abre. - Comprueba en el inspector de accesibilidad que el botón con
popovertargetexpone el estado expandido sin que tú lo declares.