Caixa de diálogo

Visão geral

Use o elemento nativo <dialog> para caixas de diálogo modais e não modais. Ele fornece a semântica de acessibilidade, o processamento de foco, a renderização da camada superior e o processamento de Esc que as sobreposições personalizadas precisam recriar manualmente.

Escolha o tipo de sobreposição

Escolha a primeira regra correspondente. Se não tiver certeza, use a Regra 1.

Regra 1: a sobreposição interrompe o usuário e bloqueia a página (padrão)

Use para confirmações, formulários, login, ações destrutivas e qualquer solicitação "modal", "caixa de diálogo", "lightbox" ou "pop-up" que não diga o contrário.

Use <dialog> aberto como um modal, de forma declarativa com command="show-modal" (consulte Controle declarativo de caixas de diálogo e pop-overs) ou de um script com showModal().

<button type="button" command="show-modal" commandfor="confirm">Delete</button>

<!-- MANDATORY: no role="dialog" or aria-modal; the native modal already provides them -->
<dialog id="confirm" aria-labelledby="confirm-title">
  <h2 id="confirm-title">Delete this item?</h2>
  <form method="dialog">
    <button value="cancel" autofocus>Cancel</button>
    <button value="delete">Delete</button>
  </form>
</dialog>

O navegador oferece: página inerte, contenção de foco, camada superior, ::backdrop, Esc e dispensa por gesto de voltar, além de retorno do foco ao invocador.

  • NÃO adicione role="dialog", aria-modal, uma armadilha de foco, alternância de inert ou um gerenciador de Esc.
  • NÃO abra com show() ou o atributo open.
  • FAÇA a rolagem da página parar atrás dele com o CSS abaixo. Consulte Impedir que a página abaixo de uma caixa de diálogo modal role para mais detalhes.
  • NÃO bloqueie a rolagem com overflow: hidden ou position: fixed em html ou body.
/* MANDATORY (both rules): without them, scrolling over the backdrop or inside the dialog scrolls the page behind it */
dialog:modal {
  overscroll-behavior: contain; /* stops scroll gestures in the dialog chaining to the page */
}
dialog::backdrop {
  overflow: hidden; /* ::backdrop is not a scroll container by default, so it would ignore overscroll-behavior */
  overscroll-behavior: contain;
}

Não use em menus ou dicas: consulte a Regra 3.

Regra 2: o usuário continua trabalhando na página enquanto o painel está aberto

Use para barras de pesquisa, inspetores, paletas e painéis de composição que ainda são caixas de diálogo semanticamente.

Use <dialog popover> para um painel de camada superior com dispensação de popover (auto dispensa leve, manual não). Use dialog.show() somente quando o painel não puder estar na camada superior.

<button type="button" popovertarget="find">Find</button>
<dialog id="find" popover="manual" aria-labelledby="find-title">
  <h2 id="find-title">Find</h2>
  <input type="search" aria-label="Search">
  <button type="button" popovertarget="find" popovertargetaction="hide">Close</button>
</dialog>

A página permanece interativa e não fica inerte.

  • É RECOMENDÁVEL incluir um botão de fechar visível.
  • NÃO use showModal(), que bloqueia a página.

Não para bloquear confirmações: consulte a Regra 1.

Regra 3: a sobreposição não é uma caixa de diálogo

Use para menus, menus suspensos, dicas, cards de passagem do cursor e notificações.

Use um elemento semântico com o atributo popover, não <dialog>.

<button type="button" popovertarget="menu">Actions</button>
<ul id="menu" popover>
  <li><a href="/rename">Rename</a></li>
</ul>

Escolha como ele pode ser dispensado

O padrão depende de como a caixa de diálogo é aberta. Se closedby estiver ausente ou for inválido, showModal() vai se comportar como closerequest, e qualquer outra forma de abertura vai se comportar como none.

  • Somente gesto de voltar e Esc (padrão modal): não defina nada ou closedby="closerequest".
  • Também clique fora para fechar: use closedby="any".
  • Somente o desenvolvedor fecha a página (por exemplo, um formulário com mudanças não salvas): use closedby="none" e adicione um caminho de confirmação. Consulte "Encerramento" abaixo.

Diretrizes

Nome acessível e foco inicial

  • É RECOMENDÁVEL nomear cada caixa de diálogo com aria-labelledby apontando para o cabeçalho ou aria-label quando não houver um título visível.
  • RECOMENDADO: coloque autofocus no controle principal ou no primeiro campo.
  • RECOMENDADO: coloque autofocus na ação segura (como "Cancelar" ou "Manter") em uma confirmação destrutiva.
  • NÃO escreva código para retornar o foco ao fechar. O navegador o retorna ao invocador.

Abertura

Encerramento

  • RECOMENDADO: feche com os botões <form method="dialog"> ou command="close".
  • FAÇA a escolha de closedby de forma deliberada: any (dispensa leve e Esc), closerequest (somente Esc, o padrão modal) ou none (somente fechamento controlado pelo desenvolvedor).
  • USE command="request-close" ou requestClose() para botões de cancelamento que precisam respeitar a lógica de mudanças não salvas. Eles acionam cancel primeiro e ignoram closedby.
  • TRATE o closedby como um aprimoramento progressivo. Esc e o gesto de voltar em dispositivos móveis já fecham os modais sem ele.

Conexão com o fechamento

  • FAÇA a leitura de dialog.returnValue em um listener close para saber qual botão foi usado e, em seguida, redefina-o com dialog.returnValue = ''. Esc e outras solicitações de fechamento não definem nenhum valor, e os navegadores diferem quanto à limpeza do valor anterior. Portanto, sem a redefinição, um valor desatualizado pode vazar. Trate '' como "dispensado sem escolha".
  • USE cancel para interceptar Esc e request-close, mas não use preventDefault() para pressionamentos repetidos de Esc sem ativação do usuário.
  • RECOMENDAMOS usar closedby="none" com uma etapa de confirmação explícita para formulários sujos.
  • RECOMENDADO: informe ao usuário por que a caixa de diálogo permaneceu aberta quando cancel é impedido. Mostre uma mensagem visível e faça referência a ela com aria-describedby no controle que recebe o foco (como "Descartar") para que um leitor de tela a leia quando o foco for movido. Sem ele, o foco que se move para um novo botão anuncia apenas o nome desse botão.
  • NÃO deixe que a confirmação mova os controles para baixo do ponteiro. Reserve o espaço da mensagem (oculte-a com visibility, não com display ou hidden) e adicione o botão destrutivo onde os botões atuais não se movem (por exemplo, primeiro em uma linha alinhada à direita) para que um segundo clique no mesmo lugar não descarte as mudanças do usuário.

Caixas de diálogo não modais

  • USE <dialog popover> (auto ou manual) para um painel de camada superior e dialog.show() quando não quiser a camada superior.
  • RECOMENDAMOS incluir um botão "Fechar" visível em todas as caixas de diálogo não modais.

Plano de fundo e estilo

Acessibilidade

  • NÃO adicione role="dialog", aria-modal, uma armadilha de foco manual ou alternância de inert a um modal nativo.
  • USE um <button> nativo com um nome acessível para controles de fechamento.
  • RECOMENDADO: use um nível de título consistente para os títulos de caixas de diálogo.

Estratégias de fallback

closedby (dialog-closedby)

Sem closedby, um modal ainda é fechado com Esc e o gesto de voltar. Somente o clique fora é perdido, então essa é uma degradação suave. closedby="none" é ignorado da mesma forma: Esc aciona cancel e fecha a caixa de diálogo. Mantenha a proteção de mudanças não salvas em um gerenciador cancel, não apenas em closedby="none", para que ela também proteja onde closedby está ausente. Se o destino de referência exigir uma dispensa leve, adicione o seguinte onde closedby estiver faltando:

if (!('closedBy' in HTMLDialogElement.prototype)) {
  // Test the pointer position, not event.target: the <dialog> is also the target for its own
  // padding and the gaps between its children, so a target check would close on those clicks
  const outside = (e, dialog) => {
    const r = dialog.getBoundingClientRect();
    return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
  };
  for (const dialog of document.querySelectorAll('dialog[closedby="any"]')) {
    let pressedOutside = false;
    dialog.addEventListener('pointerdown', (e) => { pressedOutside = outside(e, dialog); });
    // Require the press and the release outside, so a text-selection drag that ends on the backdrop does not dismiss
    dialog.addEventListener('pointerup', (e) => { if (pressedOutside && outside(e, dialog)) dialog.close(); });
  }
}

overscroll-behavior

Trate a contenção de rolagem como um aprimoramento progressivo. Não use a detecção de recursos: @supports informa que a propriedade é compatível, mesmo quando contêineres não roláveis a ignoram. Quando não é aplicada, a caixa de diálogo ainda contém rolagem sempre que o próprio conteúdo transborda, e showModal() ainda torna a página inerte. Não use overflow: hidden ou position: fixed em html ou body, porque isso remove a barra de rolagem raiz e muda o layout.

Comandos do invocador (command / commandfor)

Se o destino do Baseline não incluir comandos do invocador, mantenha a mesma marcação e carregue o polyfill de forma condicional (a mesma experiência, como aprimoramento progressivo). Detecte na interface e nunca carregue de forma incondicional:

// MANDATORY: feature detect on HTMLButtonElement.prototype and load only when native support is missing
if (!('commandForElement' in HTMLButtonElement.prototype)) {
  import('invokers-polyfill'); // npm install invokers-polyfill
}

Somente as regras 2 e 3 usam o Popover.

Alternativas e suporte do navegador para Popover

A API Popover é, em grande parte, um aprimoramento progressivo, mas os comportamentos que a definem (promoção de camada superior, fechamento leve e invocação de popovertarget) não têm equivalente somente em CSS. Os navegadores mais antigos precisam de um polyfill ou de um substituto manual, se você preferir não enviar um.

Polyfill: para oferecer suporte ao atributo popover em navegadores mais antigos, carregue condicionalmente @oddbird/popover-polyfill. OBRIGATÓRIO: detecte o recurso verificando a propriedade popover em HTMLElement.prototype e carregue o polyfill somente quando não houver suporte nativo. NÃO o carregue incondicionalmente.

Com um bundler ou mapa de importação:

// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
  import("@oddbird/popover-polyfill");
}

Sem um bundler, importe de uma CDN dentro de um <script type="module">:

<script type="module">
  if (!("popover" in HTMLElement.prototype)) {
    import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
  }
</script>

Observação sobre o estilo: o polyfill não pode definir a pseudoclasse :popover-open real. Por isso, ele aplica uma classe .\:popover-open. OBRIGATÓRIO: combine os dois com :is() ou :where(). Caso contrário, os navegadores que não têm :popover-open vão descartar toda a regra:

[popover]:is(:popover-open, .\:popover-open) {
  display: block;
}

Como alternativa, para um substituto legado sem um polyfill, use position: fixed e calcule manualmente as coordenadas usando getBoundingClientRect() ou use o posicionamento padrão com inset: auto, se isso for aceitável para o caso de uso.