Dois recursos do elemento <dialog> apareceram quase juntos e são tratados como se fossem a mesma novidade: abrir e fechar por HTML puro, com command e commandfor, e fechar clicando fora, com o atributo closedby. Os dois eliminam JavaScript de cola. Os dois aparecem no mesmo artigo, na mesma semana, na mesma newsletter.
Só que um deles é Baseline desde dezembro de 2025 e o outro tem zero versões de Safari com suporte, no desktop e no iOS. Se você escrever os dois hoje sem olhar a tabela, metade dos seus usuários fica com um modal que não fecha.
Invoker commands: esse você escreve hoje
A Invoker Commands API dá a um <button> dois atributos. O commandfor recebe o id do elemento alvo e o command diz o que fazer com ele:
<button commandfor="mydialog" command="show-modal">Abrir</button>
<dialog id="mydialog">
<p>Conteúdo</p>
<button commandfor="mydialog" command="close">Fechar</button>
</dialog>
Nenhum querySelector, nenhum addEventListener, nenhum showModal(). Para <dialog> os comandos nativos são show-modal, close e request-close. Para popover, toggle-popover, show-popover e hide-popover.
O suporte é o seguinte, e vale copiar em algum lugar:
- Chrome 135 e Edge 135, em abril de 2025
- Firefox 144, em outubro de 2025
- Safari 26.2, em dezembro de 2025
Com o Safari fechando a fila em 12 de dezembro de 2025, a feature entrou em Baseline no estado newly available. Vale ler essa etiqueta pelo que ela significa: newly available quer dizer que a última engine acabou de implementar, não que a base instalada acompanhou. Oito meses depois, quem ainda está numa versão de iOS anterior à 26.2 não tem o recurso. Para site de conteúdo isso normalmente é aceitável. Para checkout, não é.
Existe também o lado programático, com commandForElement e command como propriedades do botão, e comandos customizados prefixados com --, que disparam um CommandEvent no elemento alvo:
<button commandfor="my-img" command="--rotate-left">Girar</button>
myImg.addEventListener("command", (event) => {
if (event.command === "--rotate-left") {
myImg.style.rotate = "-90deg";
}
});
Esse pedaço é menos interessante do que parece: você trocou um listener de click no botão por um listener de command no alvo. O ganho real está nos comandos nativos, onde o listener some de vez.
closedby: esse não
O closedby resolve um problema que todo mundo já resolveu errado pelo menos uma vez, que é fechar o modal quando o usuário clica fora dele. Ele distingue três formas de fechar um dialog: o clique fora (light dismiss), a ação de plataforma (a tecla Escape no desktop, o gesto de voltar no celular) e o mecanismo que você escreveu, como um botão ou o submit de um form.
Os valores são três:
any: fecha pelas três formas
closerequest: fecha por ação de plataforma ou pelo seu mecanismo, mas não por clique fora
none: só pelo seu mecanismo
Sem o atributo, o comportamento padrão depende de como o dialog foi aberto: com showModal() ele age como closerequest; com show() ou com o atributo open, age como none. Ou seja, o clique fora nunca é o padrão. Você precisa pedir por ele.
E é aqui que a tabela estraga a festa:
- Chrome 134 e Edge 134, desde março de 2025
- Firefox 141, desde julho de 2025
- Safari: sem suporte da versão 3.1 até a 27, apenas no Technology Preview
- Safari iOS: sem suporte em nenhuma versão listada, até a 26.5
Cobertura global em torno de 71,5%, e status Baseline limited. Não é um caso de “quase lá”. É um recurso que não existe no navegador padrão de todo iPhone.
request-close é a peça que amarra os dois, e cai junto
O comando request-close merece atenção separada porque é o mais útil dos três e o mais fácil de usar errado. Diferente de close, ele pede o fechamento respeitando o closedby, em vez de fechar direto. Num formulário com dados não salvos, essa distinção é a diferença entre perguntar e perder o que o usuário digitou.
Só que ele depende do closedby para fazer sentido. Escrever command="request-close" apostando que o navegador vai respeitar um closedby="none" que ele não implementa é a combinação exata que passa em teste no Chrome e falha no Safari.
O que escrever hoje
A recomendação prática se divide em duas linhas.
Use command e commandfor como enriquecimento progressivo, sem fallback elaborado. Um botão que não entende command simplesmente não faz nada, o que é ruim, então mantenha o listener de click chamando showModal() enquanto a base instalada não virar. Os dois coexistem sem conflito, porque o navegador que entende o comando executa a ação nativa e o navegador antigo cai no listener.
Para o clique fora, continue escrevendo o fallback. O caminho conhecido é comparar o alvo do clique com o próprio dialog, aproveitando que o ::backdrop conta como área do elemento:
dialog.addEventListener("click", (event) => {
if (event.target === dialog) {
dialog.close();
}
});
Isso funciona porque o clique no backdrop tem o próprio <dialog> como target, enquanto o clique no conteúdo tem um filho. Não é bonito e tem o furo conhecido do padding no dialog, que faz a borda interna contar como fora. Mas é o que roda em iOS hoje.
A leitura de fundo é que “declarativo” e “disponível” viraram duas etiquetas diferentes na plataforma web. A quantidade de recurso novo com implementação em duas engines subiu muito, e ler a tabela de suporte deixou de ser detalhe de fim de artigo para virar o primeiro passo.
Perguntas frequentes
Dá para usar closedby com detecção de recurso?
Dá, e é o caminho limpo. Teste "closedBy" in HTMLDialogElement.prototype e só registre o listener de clique quando a propriedade não existir. Assim você não paga o custo do fallback nos navegadores que já implementam, e o comportamento fica igual nos dois.
O elemento dialog em si tem suporte amplo?
Tem. O <dialog> é Baseline widely available desde março de 2022, nas quatro engines. O que é novo, e desigual, são esses recursos em volta dele.
Isso substitui o popover?
Não. Os dois resolvem coisas diferentes: <dialog> com showModal() torna o resto da página inerte e prende o foco, que é o comportamento certo para uma decisão que bloqueia o fluxo. Popover é para conteúdo transitório que não bloqueia nada, como menu e tooltip. Os comandos invocadores atendem os dois, com verbos separados.
Fontes