O HTML 5.x e as especificações mais recentes entregaram funcionalidades que antes dependiam obrigatoriamente de JavaScript: modais nativos (<dialog>), popovers declarativos (atributo popover), disclosure widgets (<details>/<summary>), templates reutilizáveis (<template>), e outros. Usar o nativo quando possível não é apenas elegância — é acessibilidade, comportamento de teclado e foco gerenciados pelo browser, sem linha de código adicional.
<dialog> — modal nativo
O elemento <dialog> é a alternativa nativa ao padrão de div-com-role-dialog-e-focus-trap construído manualmente com ARIA (nota 08). O browser gerencia foco, sobreposição, e acessibilidade.
<dialog id="confirmar-exclusao" aria-labelledby="dialog-titulo"> <h2 id="dialog-titulo">Confirmar exclusão</h2> <p>Esta ação não pode ser desfeita.</p> <div class="actions"> <button id="btn-confirmar">Excluir</button> <button id="btn-cancelar">Cancelar</button> </div></dialog><button id="btn-abrir">Abrir diálogo</button>
const dialog = document.getElementById('confirmar-exclusao');const btnAbrir = document.getElementById('btn-abrir');const btnCancelar = document.getElementById('btn-cancelar');const btnConfirmar = document.getElementById('btn-confirmar');// showModal(): modal verdadeiro com backdrop, foco capturado, Escape fechabtnAbrir.addEventListener('click', () => dialog.showModal());// show(): diálogo não-modal (sem backdrop, sem foco capturado)// btnAbrir.addEventListener('click', () => dialog.show());btnCancelar.addEventListener('click', () => dialog.close());// close() aceita um returnValuebtnConfirmar.addEventListener('click', () => dialog.close('confirmado'));// Evento disparado quando o dialog fecha (qualquer forma)dialog.addEventListener('close', () => { console.log(dialog.returnValue); // 'confirmado' ou '' (Escape)});// Fechar clicando no backdrop (o backdrop não é filho do dialog)dialog.addEventListener('click', (e) => { if (e.target === dialog) dialog.close();});
O <dialog> com showModal() entrega automaticamente:
Top-layer: renderizado acima de qualquer z-index (sem hacks de z-index)
Foco automático: foco vai para o primeiro elemento focável dentro do dialog
Focus trap: Tab fica preso dentro do dialog
Escape fecha: comportamento nativo de teclado
Backdrop nativo: estilizável via ::backdrop pseudo-element
Prefira sempre <dialog> nativo. O padrão ARIA (nota 08) deve ser reservado para quando você está usando uma biblioteca de UI que não expõe o elemento <dialog> diretamente.
Atributo popover — tooltips e menus declarativos
O atributo popover (HTML Living Standard, 2023) permite criar popovers — elementos que aparecem sobre o conteúdo — sem JavaScript, apenas com atributos declarativos:
<!-- Controle: botão com popovertarget --><button popovertarget="meu-popover">Abrir menu</button><!-- Popover: auto fecha ao clicar fora ou apertar Escape --><div id="meu-popover" popover> <p>Conteúdo do popover</p> <button popovertargetaction="hide" popovertarget="meu-popover">Fechar</button></div>
Tipos de popover
<!-- popover="auto" (padrão): fecha ao clicar fora ("light dismiss") --><div id="menu-dropdown" popover="auto"> <ul> <li><a href="/perfil">Meu perfil</a></li> <li><a href="/config">Configurações</a></li> <li><button>Sair</button></li> </ul></div><!-- popover="manual": não fecha automaticamente — requer JS ou botão explícito --><div id="notificacao-toast" popover="manual"> <p>Item adicionado ao carrinho!</p></div>
// Controle via JavaScriptconst toast = document.getElementById('notificacao-toast');// Mostrartoast.showPopover();// Escondertoast.hidePopover();// Alternartoast.togglePopover();// Auto-fechar após 3 segundostoast.showPopover();setTimeout(() => toast.hidePopover(), 3000);// Evento disparado ao abrir/fechartoast.addEventListener('toggle', (e) => { console.log(e.newState); // 'open' ou 'closed'});
O popover também vai para o top-layer — sem problemas de z-index, sem precisar mover o elemento no DOM.
Âncora posicionada (anchor positioning)
CSS Anchor Positioning (2024) permite posicionar o popover relativo ao seu botão de controle:
/* Botão é a âncora */#btn-menu { anchor-name: --btn-menu;}/* Popover posicionado abaixo do botão */#dropdown-menu { position-anchor: --btn-menu; top: anchor(bottom); left: anchor(left); position-area: bottom span-right;}
<details> e <summary> — disclosure nativo
O par <details>/<summary> é o disclosure widget nativo — o “accordion” sem JavaScript:
<!-- Padrão básico --><details> <summary>Ingredientes</summary> <ul> <li>2 xícaras de farinha</li> <li>1 colher de fermento</li> </ul></details><!-- Aberto por padrão --><details open> <summary>Termos de uso</summary> <p>Ao usar este serviço, você concorda com...</p></details><!-- FAQ com múltiplos details --><section aria-label="Perguntas frequentes"> <details> <summary>Como cancelar minha assinatura?</summary> <p>Acesse Configurações → Assinatura → Cancelar plano.</p> </details> <details> <summary>Quais formas de pagamento são aceitas?</summary> <p>Cartão de crédito, débito e PIX.</p> </details></section>
// Detectar abertura/fechamentoconst details = document.querySelector('details');details.addEventListener('toggle', (e) => { console.log(details.open ? 'abriu' : 'fechou');});// Fechar todos os outros ao abrir um (accordion behavior)document.querySelectorAll('details').forEach(d => { d.addEventListener('toggle', () => { if (d.open) { document.querySelectorAll('details').forEach(other => { if (other !== d) other.open = false; }); } });});
O atributo for em <output> espelha o for do <label> — lista os IDs dos controles que contribuem para o resultado. Leitores de tela anunciam o <output> como uma “live region” implícita.
<meter> e <progress> — escalas e progresso
<!-- <progress>: progresso de tarefa (determinado ou indeterminado) --><!-- Determinado: valor e max conhecidos --><label for="upload">Upload:</label><progress id="upload" value="70" max="100">70%</progress><!-- Indeterminado: sem value (spinner de loading) --><progress id="carregando">Carregando...</progress><!-- <meter>: escala de medição com faixas semânticas --><!-- Uso de disco: 8GB de 20GB usados --><label for="uso-disco">Uso de disco:</label><meter id="uso-disco" value="8" min="0" max="20" low="12" high="16" optimum="4">8 de 20 GB</meter>
Atributos semânticos do <meter>:
low: abaixo deste valor é considerado baixo
high: acima deste valor é considerado alto
optimum: valor considerado ótimo
O browser aplica cores automaticamente (verde/amarelo/vermelho) baseadas nesses limiares
<datalist> combina entrada livre (o usuário pode digitar qualquer coisa) com sugestões (as <option>) — diferente de <select> que força escolha da lista. Suportado em todos os browsers modernos desde 2015.
Lazy loading nativo e Intersection Observer
loading="lazy" (nota 10) é implementado internamente pelo browser usando a Intersection Observer API — que também está disponível para JavaScript:
// Padrão básico — ideal para lazy load de componentesconst observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { if (entry.isIntersecting) { // Elemento entrou na viewport const el = entry.target; el.classList.add('visivel'); // Parar de observar após primeiro trigger observer.unobserve(el); } }); }, { rootMargin: '0px 0px 200px 0px', // Aciona 200px antes de entrar na viewport threshold: 0.1 // 10% do elemento visível });// Observar todos os elementos com data-lazydocument.querySelectorAll('[data-lazy]').forEach(el => observer.observe(el));
Resumo das APIs e seus substitutos JavaScript
flowchart LR
subgraph Nativo["HTML Nativo"]
D["<dialog>"]
POP["popover attr"]
DET["<details>"]
TEMPL["<template>"]
OUT["<output>"]
PROG["<progress>"]
METER["<meter>"]
DATA["<datalist>"]
end
subgraph JS["Substituto JavaScript (antes)"]
D1["div + ARIA + focus trap"]
POP1["div posicionado + z-index + clickOutside"]
DET1["div + aria-expanded + toggle JS"]
TEMPL1["innerHTML / createElement manual"]
OUT1["span atualizado por JS"]
PROG1["div com width: X%"]
METER1["div colorida por JS"]
DATA1["autocomplete library"]
end
D -.-> D1
POP -.-> POP1
DET -.-> DET1
TEMPL -.-> TEMPL1
OUT -.-> OUT1
PROG -.-> PROG1
METER -.-> METER1
DATA -.-> DATA1
Para fixar
Qual a diferença entre dialog.show() e dialog.showModal()? O que cada um entrega automaticamente?
Quando usar popover="auto" vs popover="manual"?
Como <details name="grupo"> muda o comportamento de um conjunto de accordions?
Por que <template> não renderiza seu conteúdo? Como acessar esse conteúdo via JavaScript?
Qual a diferença semântica entre <progress> e <meter>?
Cite uma vantagem de <dialog> nativo sobre o padrão de role="dialog" + ARIA manual.