Dialog y popover
Lección 30 de 37 · 4 min
HTML ya trae modales y popovers con el teclado resuelto. CSS se encarga de lo que faltaba: que entren y salgan animados.
Modales con dialog
El elemento <dialog> es Baseline ampliamente disponible. Se abre desde JavaScript, de dos formas: showModal() lo abre como modal y show() lo abre sin modal, sin fondo y con la página todavía usable. Con showModal() el navegador te da gratis:
- Capa superior
top layerEl modal queda encima de todo, sin pelear conz-indexni con eloverflowde sus padres. - Fondo inerte
inertEl resto de la página no recibe clics ni foco, así que Tab recorre solo el modal. - Escape
EscLo cierra, y el foco vuelve al elemento que lo abrió. - Telón
::backdropUn pseudo-elemento detrás del modal que podés oscurecer.
Un <form method="dialog"> adentro cierra el modal al enviarse, sin una línea de JavaScript. Abrí cada uno con su botón, cerralo con Escape y mirá a dónde vuelve el foco. Una transición toma la duración del estado al que va: por eso la regla [open] lleva la de apertura y la regla base la de cierre, un 60% más corta, porque cerrar no debería hacer esperar.
Lago Moraine
Banff · octubre




4 fotos en el álbum
Compartir “Lago Moraine”
.dialogo {opacity: 0;translate: 0 8px;scale: 0.96;transition-property: opacity, translate, scale;transition-duration: 150ms; /* al cerrar */transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);}.dialogo[open] {opacity: 1;translate: 0 0;scale: 1;transition-duration: 250ms; /* al abrir */}.dialogo::backdrop {background-color: rgb(0 0 0 / 0);transition-property: background-color;transition-duration: 150ms;transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);}.dialogo[open]::backdrop {background-color: rgb(0 0 0 / 0.4);transition-duration: 250ms;}/* sin soporte: abre y cierra sin animación */@supports (transition-behavior: allow-discrete) {.dialogo {transition-property: opacity, translate, scale, display, overlay;transition-behavior: allow-discrete;}.dialogo::backdrop {transition-property: background-color, display, overlay;transition-behavior: allow-discrete;}/* después de [open]: mismo peso, gana el último */@starting-style {.dialogo[open] {opacity: 0;translate: 0 8px;scale: 0.96;}.dialogo[open]::backdrop {background-color: rgb(0 0 0 / 0);}}}/* movimiento reducido: queda el fundido, sin desplazamiento */@media (prefers-reduced-motion: reduce) {.dialogo,.dialogo[open] {translate: none;scale: none;}}
<!-- se abre con dialog.showModal() --><dialogclass="m-auto rounded-xl bg-white p-4 dark:bg-neutral-900opacity-0 motion-safe:translate-y-2 motion-safe:scale-96open:opacity-100 open:translate-y-0 open:scale-100transition-[opacity,translate,scale,display,overlay] transition-discrete duration-150 open:duration-250 ease-[cubic-bezier(0.22,1,0.36,1)]starting:open:opacity-0 starting:open:motion-safe:translate-y-2 starting:open:motion-safe:scale-96backdrop:bg-black/0 backdrop:transition-[background-color,display,overlay] backdrop:transition-discrete backdrop:duration-150 open:backdrop:duration-250 backdrop:ease-[cubic-bezier(0.22,1,0.36,1)]open:backdrop:bg-black/40 starting:open:backdrop:bg-black/0"><p>Contenido</p><form method="dialog"><button>Cerrar</button></form></dialog>
El atributo popover
El atributo popover convierte cualquier elemento en un popover, y un botón con popovertarget lo abre y lo cierra sin JavaScript. Es Baseline 2025: anda en los navegadores actuales, pero conviene probarlo en los que tenga tu público.
<button popovertarget="aviso">Abrir</button><div id="aviso" popover>Contenido</div>
- Cierre ligero
popover="auto"Se cierra con un clic afuera o con Escape, y abrir otro popover auto cierra el anterior. Es lo que hacepopovera secas. - Manual
popover="manual"Solo se cierra con su botón o desde JavaScript, y pueden convivir varios abiertos. - Abierto
:popover-openSelecciona el popover mientras está abierto, como[open]en el modal.
El popover también va a la capa superior, pero no es modal: la página sigue usable. Usá el modal cuando la persona tiene que responder antes de seguir (confirmar, completar un formulario) y el popover para menús, ayudas y avisos que se pueden ignorar.
Animar la entrada y la salida
El problema clásico: un elemento cerrado tiene display: none, y display no se transicionaba. Dos piezas Baseline 2024 lo resuelven. @starting-style define el estilo desde el que arranca la entrada, y transition-behavior: allow-discrete deja que display espere a que termine la salida. Sumá overlay para que el elemento no salga de la capa superior antes de tiempo; tiene disponibilidad limitada, así que donde no existe la salida pierde parte del efecto.
.dialogo {opacity: 0;transition-property: opacity, display, overlay;transition-duration: 200ms;transition-behavior: allow-discrete;}.dialogo[open] {opacity: 1;}@starting-style {.dialogo[open] {opacity: 0;}}
- El orden importa
@starting-styleVa después de la regla[open], porque tienen el mismo peso y gana la última. - Ojo con el atajo
transitionDevuelvetransition-behavioranormal: escribíallow-discretedespués, o adentro del atajo. - Mejora progresiva
@supportsSin soporte, el modal abre y cierra sin animación. Por eso el CSS del demo agrupa lo nuevo en@supports.
Con prefers-reduced-motion: reduce, dejá el fundido y sacá el desplazamiento y la escala. Y si ves :open, es Baseline 2026: hace lo mismo que [open], que hoy anda en todos lados.
En Tailwind
Tailwind v4 tiene una variante para cada pieza. open: cubre [open], :popover-open y :open a la vez, así que las mismas clases sirven para el modal y para el popover.
| CSS | Tailwind |
|---|---|
| [open] · :popover-open | open: |
| @starting-style | starting:open:opacity-0 |
| transition-behavior: allow-discrete | transition-discrete |
| transition-property: opacity, display, overlay | transition-[opacity,display,overlay] |
| ::backdrop | open:backdrop:bg-black/40 |
| prefers-reduced-motion: no-preference | motion-safe: |
<button popovertarget="aviso">Abrir</button><div id="aviso" popoverclass="m-auto rounded-xl bg-white p-4opacity-0 open:opacity-100 starting:open:opacity-0transition-[opacity,display,overlay] transition-discrete duration-200">Contenido</div>
Dialog and popover
Lesson 30 of 37 · 4 min
HTML already ships modals and popovers with the keyboard handled. CSS takes care of what was missing: animating them in and out.
Modals with dialog
The <dialog> element is Baseline widely available. You open it from JavaScript, in two ways: showModal() opens it as a modal, and show() opens it without a modal, with no backdrop and the page still usable. With showModal() the browser gives you for free:
- Top layer
top layerThe modal sits above everything, with no fightingz-indexor its parents'overflow. - Inert background
inertThe rest of the page gets no clicks or focus, so Tab only moves through the modal. - Escape
EscCloses it, and focus goes back to the element that opened it. - Backdrop
::backdropA pseudo-element behind the modal that you can dim.
A <form method="dialog"> inside closes the modal when submitted, without a line of JavaScript. Open each one with its button, close it with Escape and watch where focus goes. A transition takes its duration from the state it goes to: that's why the [open] rule carries the opening time and the base rule the closing one, 60% shorter, because closing shouldn't keep anyone waiting.
Moraine Lake
Banff · October




4 photos in the album
Share “Moraine Lake”
.dialog {opacity: 0;translate: 0 8px;scale: 0.96;transition-property: opacity, translate, scale;transition-duration: 150ms; /* while closing */transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);}.dialog[open] {opacity: 1;translate: 0 0;scale: 1;transition-duration: 250ms; /* while opening */}.dialog::backdrop {background-color: rgb(0 0 0 / 0);transition-property: background-color;transition-duration: 150ms;transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);}.dialog[open]::backdrop {background-color: rgb(0 0 0 / 0.4);transition-duration: 250ms;}/* no support: opens and closes without animation */@supports (transition-behavior: allow-discrete) {.dialog {transition-property: opacity, translate, scale, display, overlay;transition-behavior: allow-discrete;}.dialog::backdrop {transition-property: background-color, display, overlay;transition-behavior: allow-discrete;}/* after [open]: same specificity, the last one wins */@starting-style {.dialog[open] {opacity: 0;translate: 0 8px;scale: 0.96;}.dialog[open]::backdrop {background-color: rgb(0 0 0 / 0);}}}/* reduced motion: keep the fade, drop the movement */@media (prefers-reduced-motion: reduce) {.dialog,.dialog[open] {translate: none;scale: none;}}
<!-- opened with dialog.showModal() --><dialogclass="m-auto rounded-xl bg-white p-4 dark:bg-neutral-900opacity-0 motion-safe:translate-y-2 motion-safe:scale-96open:opacity-100 open:translate-y-0 open:scale-100transition-[opacity,translate,scale,display,overlay] transition-discrete duration-150 open:duration-250 ease-[cubic-bezier(0.22,1,0.36,1)]starting:open:opacity-0 starting:open:motion-safe:translate-y-2 starting:open:motion-safe:scale-96backdrop:bg-black/0 backdrop:transition-[background-color,display,overlay] backdrop:transition-discrete backdrop:duration-150 open:backdrop:duration-250 backdrop:ease-[cubic-bezier(0.22,1,0.36,1)]open:backdrop:bg-black/40 starting:open:backdrop:bg-black/0"><p>Content</p><form method="dialog"><button>Close</button></form></dialog>
The popover attribute
The popover attribute turns any element into a popover, and a button with popovertarget opens and closes it without JavaScript. It's Baseline 2025: it works in current browsers, but test it in the ones your audience uses.
<button popovertarget="notice">Open</button><div id="notice" popover>Content</div>
- Light dismiss
popover="auto"A click outside or Escape closes it, and opening another auto popover closes the previous one. It's what plainpopoverdoes. - Manual
popover="manual"Only closes with its button or from JavaScript, and several can stay open together. - Open
:popover-openSelects the popover while it's open, like[open]on the modal.
The popover also goes to the top layer, but it isn't modal: the page stays usable. Use the modal when the person has to answer before moving on (confirm, fill in a form) and the popover for menus, hints and notices they can ignore.
Animating the entry and exit
The classic problem: a closed element has display: none, and display didn't transition. Two Baseline 2024 pieces solve it. @starting-style sets the style the entry starts from, and transition-behavior: allow-discrete lets display wait for the exit to finish. Add overlay so the element doesn't leave the top layer too early; it has limited availability, so where it's missing the exit loses part of the effect.
.dialog {opacity: 0;transition-property: opacity, display, overlay;transition-duration: 200ms;transition-behavior: allow-discrete;}.dialog[open] {opacity: 1;}@starting-style {.dialog[open] {opacity: 0;}}
- Order matters
@starting-styleIt goes after the[open]rule, because they have the same specificity and the last one wins. - Mind the shorthand
transitionIt resetstransition-behaviortonormal: writeallow-discreteafter it, or inside the shorthand. - Progressive enhancement
@supportsWithout support, the modal opens and closes without animation. That's why the demo's CSS groups the new parts in@supports.
With prefers-reduced-motion: reduce, keep the fade and drop the movement and the scale. And if you see :open, it's Baseline 2026: it does the same as [open], which works everywhere today.
In Tailwind
Tailwind v4 has a variant for each piece. open: covers [open], :popover-open and :open at once, so the same classes work for the modal and the popover.
| CSS | Tailwind |
|---|---|
| [open] · :popover-open | open: |
| @starting-style | starting:open:opacity-0 |
| transition-behavior: allow-discrete | transition-discrete |
| transition-property: opacity, display, overlay | transition-[opacity,display,overlay] |
| ::backdrop | open:backdrop:bg-black/40 |
| prefers-reduced-motion: no-preference | motion-safe: |
<button popovertarget="notice">Open</button><div id="notice" popoverclass="m-auto rounded-xl bg-white p-4opacity-0 open:opacity-100 starting:open:opacity-0transition-[opacity,display,overlay] transition-discrete duration-200">Content</div>