Como usar e estilizar Dialog
Não foi incrível o dia em que a Web ganhou um elemento <dialog> nativo em HTML? E é bom relembrar como usar e estilizar o elemento dialog e ter um guia como este para consulta.
Marcando um <dialog>
Esta é a marcação básica:
<button id="dialog-button">Open Dialog</button>
<dialog id="dialog">...</dialog>Ele não abre por padrão. Seria possível definir manualmente o atributo open:
<dialog id="dialog-button" open>...</dialog>Mas quando é que se quer abrir um dialog por padrão? Certamente é um caso de uso raro. Em vez disso, existe um método JavaScript show() para isso.
Basta criar as variáveis para o dialog e para o botão e, então, invocar o método:
const dialogButton = document.querySelector('#dialog-button');const dialog = document.querySelector('#dialog');
dialogButton.addEventListener('click', () => { dialog.show();})Isso funciona, mas atenção: esse método trata o dialog mais como um pop-up do que como um modal, e modal é o que se vai querer na maioria dos casos.
As diferenças entre um e outro? Um modal inclui um backdrop, é posicionado automaticamente no centro da página e permite que a tecla Esc o feche.
Repare como o show() não tem backdrop, posicionamento nem o comportamento de fechamento:
Então, talvez seja melhor invocar o método showModal():
const dialogButton = document.querySelector('#dialog-button');const formDialog = document.querySelector('#dialog');
dialogButton.addEventListener('click', () => { formDialog.showModal();})Ótimo! Ele abre, se posiciona… e fecha:
Mais sobre o fechamento
Agora é possível apertar a tecla Esc enquanto o dialog está em foco (e repare que ele fica em foco por padrão) para fechá-lo. Mas, se a ideia é ter alguma UI que feche o dialog, como um botão, dá para adicioná-la dentro do elemento:
<button id="dialog-button">Open Dialog</button>
<dialog id="dialog"> <button id="dialog-close">Close</button> <!-- etc. --></dialog>Isso não funciona de imediato, mas provavelmente já dá para adivinhar como resolver. Existe um método close() para isso:
const formButton = document.querySelector('#dialog-button');const formDialog = document.querySelector('#dialog');const formClose = document.querySelector('#dialog-close');
formButton.addEventListener('click', () => { formDialog.showModal();})
formClose.addEventListener('click', () => { formDialog.close();})É um pouco engraçado/estranho que não exista um closeModal() correspondente. Sem problema, porém, porque o close() simplesmente funciona:
E, para quem quer uma abordagem sem JavaScript, dá para fazer isso declarativamente, direto no HTML:
<dialog id="dialog"> <form method="dialog"> <button type="submit">Close dialog</button> </form></dialog>Não dá para garantir como isso afeta a semântica, mas é realmente possível fechar o danado sem JavaScript.
Invoker commands
Já que o assunto é fechamento declarativo, vale mencionar um recurso em evolução chamado invoker commands, projetado especificamente para abrir e fechar dialogs de forma declarativa. Tudo ainda é totalmente experimental no momento em que este artigo foi escrito, mas veja só:
<button command="show-modal" commandfor="my-dialog">Show Dialog</button>
<dialog id="my-dialog">...</dialog>Isso mesmo! Será possível conectar o botão ao dialog diretamente no HTML, com os atributos command e commandfor, para invocar um dialog específico (#my-dialog) e aplicar show-modal.
E o mesmo vale para um botão de fechar:
<dialog id="my-dialog"> <!-- Close #my-dialog --> <button command="close" commandfor="my-dialog">Close Dialog</button></dialog>E, como Danny explica aqui, é possível conectar tudo isso com JavaScript, caso seja necessário escutar esses comandos e disparar algum evento quando eles acontecerem:
// Seleciona todos dialogsconst dialogs = document.querySelectorAll("dialog");
// Loop em todos dialogsdialogs.forEach(dialog => {
// Escuta por close dialog.addEventListener("close", () => { // Dialog fechado });
// Escuta por command dialog.addEventListener("command", event => {
// Se command é show-modal if (event.command == "show-modal") { // Dialog é mostrado (modal) }
// Outro jeito de escutar close else if (event.command == "close") { // Dialog é fechado }
});});Fique de olho no suporte!
Sobre o rótulo dos botões
Pode ser tentador usar um “X” (ou ao menos um ícone SVG dele) como rótulo do botão de fechar:
<button id="dialog-button">Open Dialog</button>
<dialog id="dialog"> <button id="dialog-close">X</button></dialog>Mas isso não é exatamente a melhor coisa para os leitores de tela anunciarem. Ainda se quer que seja anunciado “Fechar dialog” ou algo nesse sentido.
Então, para quem faz questão de usar um ícone “X”, vale adicionar um <span> contendo o texto que se quer ler e escondê-lo visualmente, ao mesmo tempo em que se impede que o ícone seja anunciado, usando o atributo aria-hidden:
<button id="form-button">Open Dialog</button>
<dialog id="form-dialog"> <button id="form-close"> <span class="visually-hidden">Close modal</span> <span aria-hidden="true"></span> </button></dialog>Vale a pena conferir também por que ícones precisam de labels em interfaces.
Mais uma observação com foco em acessibilidade. Percebeu como o botão de fechar recebe foco quando o dialog é aberto?

Isso pode ou não ser desejável, porque agora o botão pode fechar o dialog inesperadamente caso a tecla Space seja acionada por acidente. Não é o fim do mundo, já que fechar um modal não é exatamente algo destrutivo e ele pode ser aberto novamente.
Mas, se houver outros elementos focáveis no dialog (talvez um link ou um campo de formulário), então, talvez valha considerar dar o foco inicial a um deles com o atributo tabindex.
Inércia inata
O próximo assunto é a estilização do backdrop, mas antes disso vale notar que a página atrás de um dialog aberto fica inerte. Em outras palavras, qualquer tipo de interação (seleção de texto, clique em botões, foco, inputs etc.) fica indisponível.
Isso é provavelmente o que se quer de qualquer forma, então é bom que não precise ser configurado. Só não se vai enxergar o atributo inert na marcação quando isso acontecer.
Mas isso só vale quando o dialog está configurado como modal. Lembra da primeira demo? Nela, foi usado o método show() para abrir o dialog no clique, em vez do mais explícito showModal().
Isso significa que a primeira demo mostra algo mais parecido com um popover (pense em tooltips) do que com um verdadeiro modal que rouba a atenção, prende o foco e fica na camada mais alta (top layer).
Você pode estar se perguntando sobre dialogs concorrentes, como um popover e um modal abertos ao mesmo tempo. Bem, como eles foram abertos ao mesmo tempo, para começo de conversa?
Seria preciso abrir primeiro o dialog popover, já que ele não dispara o comportamento de inert. Só o dialog modal faz isso. E, quando o dialog modal está aberto, o dialog popover não está no top layer, o que o torna inacessível.
Enfim, vamos à estilização!
Estilizando o backdrop
Vale começar por aqui porque é incrivelmente difícil sequer enxergar o backdrop do jeito que ele vem estilizado por padrão. Ao abrir o dialog do último exemplo, repare que o fundo da página fica levemente mais escuro. Aquilo é o backdrop.

Bem sutil. Dá para estilizá-lo com o pseudo-elemento ::backdrop. Por exemplo, pode-se ir de cor sólida total:
Maaaas aí a página inteira atrás dele fica escurecida. Isso pode até ser aceitável, mas um pouco de transparência, talvez com uma dose de blur(), não faz mal:
O exemplo de imagem de fundo do Mojtaba, no Almanac do CSS-Tricks, é muito bom (mesmo contradizendo a ressalva sobre escurecer o restante da página):
Estilizando a borda e o fundo
Dois padrões bem óbvios definem a aparência do <dialog>: um fundo branco baunilha e uma bela borda preta. É totalmente aceitável deixar como está, se for essa a intenção. Ou não.

Pode parecer que os estilos customizados devam ir direto no elemento <dialog>:
/* 👎 */dialog { background-color: gold; border: 0; border-radius: 12px;}Mas, na verdade, o que se quer é selecioná-lo no estado open:
dialog { /* ... */
&[open] { background-color: gold; border: 0; border-radius: 12px; }}Talvez você tenha notado, na captura do DevTools ali em cima, que a pseudo-classe :modal tem especificidade ainda maior que a :open. Ela também pode ser usada, caso sejam necessárias sobrescritas das sobrescritas.
Atenção com a pseudo-classe :open, porém. O Safari 26.5 acabou de ganhar suporte a ela no dia em que o artigo original foi escrito. Se for preciso um suporte mais amplo, considere selecionar o atributo [open]… ou simplesmente usar :modal.
Estilizando a posição
Um estilo padrão menos óbvio do dialog é como ele é posicionado no centro do viewport. Abra o DevTools e verá o estilo de user agent responsável por isso:

Dá para sobrescrever o margin-top para deixar o dialog um pouco mais próximo do topo do viewport:
Uma coisa que provavelmente não se deve fazer é sobrescrever o display do dialog. Ele é definido como display: none no estado fechado inicial. Se isso virar algo como block no próprio elemento, perde-se completamente o sentido de ter um modal (justamente aquele comportamento de ficar fechado por padrão).
A abertura e o fechamento básicos continuam funcionando, só que sem a conveniência da tecla Esc.
E repare como os estilos customizados só são aplicados no estado :open, já que é lá que eles vivem:
Impedindo a rolagem quando aberto
É bem provável que não se queira que o conteúdo atrás do ::backdrop role. É uma daquelas situações em que a pessoa pode ser tirada do contexto e levada para um lugar totalmente diferente da página, em relação a onde estava quando abriu o dialog.
Seria muito bom se o conteúdo subjacente ficasse travado no lugar por padrão, mas é perfeitamente compreensível que não seja assim: um dialog não é um contêiner de rolagem. Se fosse, bastaria jogar um overscroll-behavior: contain nele e pronto.
Pois bem, acontece que o Chrome 144 ajustou isso um pouco, de modo que o overscroll-behavior passa a funcionar em contêineres de rolagem não roláveis. Assim, presumindo Chrome 144 ou superior, dá para definir esse comportamento no dialog e no seu backdrop:
dialog { overscroll-behavior: contain;
&::backdrop { overscroll-behavior: contain; }}A última peça que falta é transformar o próprio dialog em um contêiner de rolagem:
dialog { overflow: hidden; overscroll-behavior: contain;
&::backdrop { overscroll-behavior: contain; }}Requer Chrome 144 ou superior:
Isso é bacana, mas outra forma (e mais concisa) de fazer, com suporte amplo nos navegadores, é verificar se o elemento body :has() um dialog com o atributo open. E, se tiver, esconde-se o overflow do body:
body:has(dialog[open]) { overflow: hidden}Dito isso, a abordagem com overscroll-behavior agrada mais, por ser mais declarativa e por estar atrelada ao elemento que está sendo selecionado.
Sendo criativo com a estilização de dialogs
Andy Clarke tem um material completo sobre formas criativas de estilizar dialogs que vão além do básico conteúdo-dentro-de-caixa. Está bem fora do escopo do que se cobre aqui, mas vale muito a leitura. Eis um exemplo para abrir o apetite:
Animando dialogs
Dialogs meio que “estalam” ao entrar e sair quando são abertos e fechados. Mas dá para acrescentar uma pitada de animação na entrada e na saída de cena.
Tipo, e se o dialog fosse aparecer lentamente, com um fade in? Pode parecer que isto funcionaria:
/* Nope! 👎 */dialog { opacity: 0; overflow: hidden; overscroll-behavior: contain; transition: opacity .5s ease-in-out; width: 80vw;
&:open { opacity: 1; }}Mas não. É preciso definir explicitamente um starting style para os elementos no exato momento em que eles são renderizados no DOM. Nesse caso, um dialog é display: none por padrão e não tem opacity definida quando é ativado. É aí que entra a at-rule @starting-style:
/* Yep! 👍 */@starting-style { dialog:open { opacity: 0; }}
dialog { overflow: hidden; overscroll-behavior: contain; transition: opacity .5s ease-in-out; width: 80vw;
&:open { opacity: 1; }}Assim, sim:
Entrar e sair de cena? Isso soa como território nobre da View Transitions API! Mas, infelizmente, dialogs não são um bom caso de uso para elas. Por quê? Dialogs modais vivem no top layer, e fechá-los pode removê-los de um jeito que nem sempre produz um par antigo/novo confiável para a transição nomeada.
Eis um exemplo em que há um view-transition-name definido no elemento dialog e, então, os estados ::view-transition-new() e ::view-transition-old() atrelados a esse nome, cada um chamando uma animação que desliza para dentro e para fora, respectivamente.
Funciona bem para a transição de entrada, mas nem tanto para a de saída. Repare também que o backdrop exige trabalho extra, já que ele entra na jogada:
O que dá para fazer, em vez disso, é algum tipo de abordagem híbrida: definir a view transition no estado de abertura e usar uma animação CSS no estado de fechamento.
Ou, quem sabe, simplesmente usar animações/transições CSS para os dois estados! Não parece haver valor real em usar uma view transition em apenas um dos estados só pelo prazer de usar uma view transition.
De qualquer forma, para quem quer ser mais criativo com animações de entrada e saída, Chris Coyier tem uma bem legal em que o modal segue um caminho definido por shape(). A demonstração dele usa um dialog configurado como popover, então o autor do artigo original fez um fork e usou um modal:
Dialog ou popover?
Qual dos dois usar? É uma pergunta muito boa, porque a Dialog API e a Popover API são superparecidas, mas foram projetadas para casos de uso diferentes. Zell Liew tem uma resposta concisa:
Depois de
um pouco demuita pesquisa, descobri que a Popover API e a Dialog API são absurdamente diferentes em termos de acessibilidade. Então, se você está tentando decidir entre usar a Popover API ou a API do Dialog, a recomendação é:
- Use a Popover API para a maioria dos popovers.
- Use a API do Dialog apenas para dialogs modais.
O “em termos de acessibilidade” é o que realmente importa aqui, porque popovers não têm:
- gerenciamento automático de foco e
- conexão automática de ARIA.
Enquanto isso, um dialog:
- torna automaticamente os outros elementos
inert, - impede que as pessoas naveguem por Tab até outros elementos e
- impede que leitores de tela alcancem outros elementos.
Portanto, para quem planeja usar um popover e precisa de recursos acessíveis para prender o foco e tornar outros elementos inertes, será preciso cuidar disso por conta própria, no JavaScript.
Zell também nota que popovers precisam de uma role acessível explícita. E há várias entre as quais escolher, então vai exigir um pouco de reflexão para acertar na escolha.
Nada disso quer dizer ei, use sempre um dialog. A questão é escolher a API certa para o caso de uso certo. Citando Zell novamente:
- Popover é um termo guarda-chuva para qualquer tipo de popup sob demanda.
- Dialog é um tipo de popover — um tipo que cria uma nova janela (ou card) para conter algum conteúdo.
Conclusão
Basicamente, é assim que se faz para usar e estilizar elementos <dialog> da maneiar mais direta possível.
Vale acompanhar a evolução do suporte dos navegadores e, se você tiver algo a acrescentar ou uma forma melhor e mais precisa de articular o que está aqui, é sempre bem-vindo.