HTML por dentro: a estrutura que sustenta cada página da web
Uma imersão no HTML: parser, DOM, semântica, acessibilidade, formulários, renderização e segurança, para páginas que funcionam para pessoas e buscadores.

Toda página da web, por mais sofisticada que pareça, começa como um arquivo de texto com marcações. O painel de um sistema de gestão, a loja virtual, o portal do banco e este artigo que você está lendo chegam ao navegador como HTML, e é a partir dele que todo o resto acontece: o CSS dá a aparência, o JavaScript dá o comportamento, mas a estrutura, o significado e boa parte da acessibilidade vêm do HTML.
Justamente por parecer simples, o HTML costuma ser tratado como um detalhe. O resultado aparece em páginas que não funcionam com teclado, formulários que o leitor de tela não consegue ler, conteúdos que o Google não indexa e botões que, na verdade, são caixas que fingem ser botões. Este artigo mostra o que existe por baixo das tags: como o navegador transforma texto numa árvore de objetos e depois em pixels, por que a semântica importa tanto e como escrever um HTML que trabalha a seu favor.
O que é o HTML
HTML, HyperText Markup Language, é uma linguagem de marcação: ela não executa nada, não tem variáveis nem laços. Ela descreve um documento, dizendo o que cada parte do conteúdo é. Isto é um título, isto é um parágrafo, isto é uma lista, isto é um link, isto é um campo de formulário.
As peças básicas são poucas:
- Elemento: uma unidade do documento, normalmente com uma tag de abertura e uma de fechamento, como
<p>e</p>, e o conteúdo entre elas. - Atributo: uma configuração do elemento, escrita na tag de abertura, como o
hrefde um link ou oaltde uma imagem. - Elementos vazios: alguns elementos não têm conteúdo nem tag de fechamento, como
<img>,<input>,<br>e<meta>. - Aninhamento: elementos ficam dentro de outros, formando uma hierarquia. Um
<li>mora dentro de um<ul>, que pode morar dentro de um<nav>.
A web moderna divide o trabalho em três camadas. O HTML cuida da estrutura e do significado; o CSS, da apresentação; o JavaScript, do comportamento. Essa separação não é dogma: é o que permite trocar o visual inteiro de um site sem tocar no conteúdo e o que faz uma página continuar compreensível quando o CSS ou o JavaScript falham.
Uma história curta
Em 1991, no CERN, Tim Berners-Lee publicou um documento chamado "HTML Tags", com cerca de 18 elementos: títulos, parágrafos, listas e, o mais importante, o link, que ligava um documento a outro em qualquer computador da rede. A ideia de hipertexto não era nova, mas a combinação de HTML, URLs e HTTP criou a web.

O HTML 2.0, de 1995, foi a primeira especificação formal e já trazia formulários. Vieram depois as versões do W3C: o HTML 3.2, em 1997, com tabelas, e o HTML 4.01, em 1999, que separou de vez a apresentação, entregue ao CSS, da estrutura. Era a época da guerra dos navegadores, em que cada fabricante inventava as suas próprias tags e cada página "funcionava melhor" num navegador específico. O Netscape criou o <blink>, que fazia o texto piscar; a Microsoft respondeu com o <marquee>, que o fazia correr pela tela; e o <font>, com cor, tamanho e família da letra escritos em cada trecho, espalhava a aparência por todo o documento. Mudar a cor dos títulos de um site significava editar cada página, uma a uma. O CSS e a separação entre estrutura e apresentação nasceram como resposta a esse caos.
Em 2000, o W3C apostou no XHTML, o HTML com as regras rígidas do XML: toda tag fechada, tudo em minúsculas, e um único erro de sintaxe impedindo a página de aparecer. A versão seguinte, o XHTML 2, quebrava a compatibilidade com a web existente. Em 2004, Apple, Mozilla e Opera, insatisfeitas com esse rumo, criaram o WHATWG e começaram a evoluir o HTML de forma compatível com o que já existia, com foco em aplicações web.
Esse trabalho virou o HTML5, recomendação do W3C em 2014, com vídeo e áudio nativos, o canvas, novos elementos semânticos, novos tipos de campo e, menos visível e muito importante, um algoritmo de leitura padronizado, que define inclusive o que fazer com HTML malformado. Em 2019, W3C e WHATWG fecharam um acordo: existe uma única especificação, o HTML Living Standard, mantida pelo WHATWG e atualizada continuamente. Não há mais "HTML6": o HTML é o que a especificação viva diz hoje.
Anatomia de um documento
Um documento HTML mínimo, e correto, tem esta forma:
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pedido 1042 | Loja Exemplo</title>
<meta name="description" content="Acompanhe o pedido 1042: itens, valores e entrega.">
<link rel="canonical" href="https://loja.exemplo.com.br/pedidos/1042">
<link rel="stylesheet" href="/estilos.css">
</head>
<body>
<main>
<h1>Pedido 1042</h1>
<p>Status: aberto</p>
</main>
<script type="module" src="/app.js"></script>
</body>
</html>
Cada linha tem um motivo:
- O **
<!doctype html>** não é decoração. Sem ele, os navegadores entram no modo de compatibilidade (quirks mode), que imita bugs de navegadores dos anos 1990 para não quebrar páginas antigas. O layout passa a se comportar de forma diferente, e ninguém quer isso em código novo. - O **
lang** diz o idioma do conteúdo. O leitor de tela escolhe a voz e a pronúncia por ele, o tradutor automático sabe de onde partir, e a hifenização do CSS funciona. - O **
<meta charset="utf-8">** precisa estar logo no início do arquivo, nos primeiros 1.024 bytes, para que acentos e símbolos sejam lidos corretamente. - O viewport faz a página usar a largura real da tela no celular, em vez de simular uma tela de computador e encolher tudo.
- O **
<title>** aparece na aba, no histórico, nos favoritos e como título no resultado de busca. A **description** costuma ser o texto exibido abaixo dele. O **canonical** diz qual é o endereço oficial daquele conteúdo quando ele pode ser acessado por mais de uma URL.
O <head> guarda metadados, que não aparecem na página; o <body>, o conteúdo. Alguns atributos valem para qualquer elemento, os chamados atributos globais: id, class, lang, title, hidden, tabindex e os data-*, que guardam dados próprios da aplicação, como data-pedido="1042".
Idioma, tradução e direção do texto
O lang do <html> vale para a página inteira, mas pode ser repetido em qualquer elemento. Uma citação em inglês no meio de um texto em português, marcada com <blockquote lang="en">, é lida pelo leitor de tela com a pronúncia do inglês, em vez de soar como português lido com as regras erradas.
O atributo **translate="no"** avisa os tradutores automáticos de que um trecho não deve ser traduzido. Nomes de produtos e de marcas são o caso clássico: um tradutor pode transformar o nome de um aplicativo numa expressão comum e deixar a frase sem sentido. Segundo a MDN, nem todos os navegadores reconhecem o atributo, mas sistemas de tradução automática como o Google Tradutor o respeitam; ele também serve para trechos de código e nomes próprios.
<p>Baixe o aplicativo <span translate="no">Pedido Fácil</span> e acompanhe as entregas.</p>
Para idiomas escritos da direita para a esquerda, como o árabe e o hebraico, existem o atributo dir e o elemento <bdi>. O <bdi> isola um trecho de direção desconhecida, como um nome digitado por um usuário, para que ele não embaralhe a ordem do texto ao redor; ele se comporta como um <span dir="auto">, em que o navegador deduz a direção pelo próprio conteúdo. Num sistema brasileiro isso parece distante, até o dia em que um cliente estrangeiro se cadastra com o nome no alfabeto dele e, numa lista como "Nome - 1º lugar", a posição e o nome aparecem trocados de lugar.
Do texto à árvore: o DOM
O navegador não trabalha com o texto do HTML. Ele o lê e constrói uma árvore de objetos na memória, o DOM (Document Object Model). Cada elemento vira um nó; o texto dentro dos elementos vira nós de texto; os atributos ficam ligados aos seus elementos.

O DOM é o que o CSS estiliza e o que o JavaScript manipula. Quando um script faz document.querySelector('main a') e muda o textContent, ele não edita o arquivo HTML: altera um objeto da árvore, e o navegador atualiza a tela. Por isso o "código-fonte da página" (o que o servidor enviou) e a aba Elementos das ferramentas de desenvolvedor (o DOM atual) podem ser muito diferentes numa aplicação que monta a tela com JavaScript.
Um parser que perdoa
Uma das características mais marcantes do HTML é que ele quase nunca dá erro. Um <p> sem fechamento, uma tabela com conteúdo no lugar errado, atributos sem aspas: o navegador corrige e segue em frente. Desde o HTML5, essa correção é definida em detalhes pela especificação, então todos os navegadores modernos constroem a mesma árvore a partir do mesmo HTML malformado.
Essa tolerância foi decisiva para o sucesso da web, mas tem um custo: o erro não aparece, ele só produz uma árvore diferente da que você imaginou. Um <div> dentro de um <p>, por exemplo, faz o parser fechar o parágrafo antes do <div>, e o CSS que dependia daquela estrutura deixa de funcionar. O validador do W3C, as ferramentas de desenvolvedor e um bom linter apontam esses casos.
Modelos de conteúdo: o que pode ficar dentro do quê
O caso do <div> dentro do <p> não é capricho do parser. A especificação define, para cada elemento, um modelo de conteúdo, a lista do que ele pode conter, e para isso agrupa os elementos em categorias. O conteúdo de fluxo reúne quase tudo o que pode ir no <body>. O conteúdo de frase (phrasing content) é o texto e as marcações que vivem dentro de uma linha, como <em>, <span>, <img> e <input>. O conteúdo interativo reúne o que foi feito para a pessoa usar, como <button>, <select>, <textarea>, <details> e o <a> com href. Há ainda as categorias de metadados, de seção, de título e de conteúdo incorporado, como <video>, <iframe> e <svg>.
A regra do parágrafo sai daí: o <p> só aceita conteúdo de frase, enquanto o <div> aceita conteúdo de fluxo. Um <div> dentro de um <p> é inválido, e o parser resolve a situação fechando o parágrafo.
O <a> tem um modelo curioso, chamado transparente: ele aceita o que o elemento pai dele aceitaria, desde que nenhum descendente seja conteúdo interativo, outro <a> ou um elemento com tabindex. Por isso um link pode envolver um cartão inteiro, com título e parágrafo, quando está dentro de um <div>, mas não pode conter um botão. A proibição faz sentido para quem usa a página: um botão dentro de um link cria dois controles empilhados no mesmo lugar, e nem o teclado nem o leitor de tela têm como saber qual deles a pessoa quis acionar. Num cartão de produto com um link para a página do produto e um botão "Adicionar ao carrinho", os dois ficam lado a lado, nunca um dentro do outro. Se o cartão inteiro precisa ser clicável, o link fica no título, e o CSS estende a área de clique dele com um pseudoelemento que cobre o cartão.
Por dentro do parser: tokens e modos de inserção
A leitura acontece em duas máquinas encadeadas. A primeira, o tokenizador, percorre o texto caractere por caractere e passa por dezenas de estados: "dados", "abertura de tag", "nome da tag", "antes do nome do atributo", "valor do atributo entre aspas" e assim por diante. Cada vez que reconhece uma peça completa, emite um token: uma tag de abertura com os seus atributos, uma tag de fechamento, um trecho de texto, um comentário.
A segunda, a construção da árvore, recebe os tokens e decide onde cada um entra. Ela trabalha com uma pilha dos elementos abertos e com mais de vinte modos de inserção, como "antes do head", "no head", "no body" e "na tabela", e cada modo tem as suas regras para cada token. É nessa etapa que mora a correção dos erros.

Alguns comportamentos surpreendem quem nunca olhou por dentro:
- Elementos implícitos. Um arquivo com só
<p>Oláproduz um DOM completo, com<html>,<head>e<body>, porque o parser cria os que faltam. Da mesma forma, uma tabela escrita com<tr>direto dentro de<table>ganha um<tbody>no DOM, e um seletor CSS comotable > trnão encontra nada. - Fechamentos automáticos. Um
<li>novo fecha o anterior, e um<p>aberto é fechado por qualquer elemento de bloco, como o<div>do exemplo anterior. - Conteúdo realocado. Texto ou elementos soltos dentro de uma tabela, fora de uma célula, são movidos para antes da tabela, um processo que a especificação chama de foster parenting. É por isso que um aviso escrito no lugar errado aparece acima da tabela, e não dentro dela.
- Texto cru. Dentro de
<script>e<style>, o tokenizador não procura tags; ele só para quando encontra o fechamento correspondente. Um</script>dentro de uma string JavaScript encerra o script ali mesmo, e o resto vira HTML. Esse detalhe é a origem de uma classe inteira de falhas de segurança, como veremos. - **
<template>.** O conteúdo de um<template>é lido, mas fica guardado à parte, inerte: imagens não carregam, scripts não rodam. Ele serve de molde para o JavaScript clonar quando precisar.
Por fim, o tokenizador decodifica as referências de caractere, as entidades: < vira <, & vira &, vira um espaço que não quebra linha e — vira o travessão. Há mais de duas mil referências com nome, mas, com o arquivo em UTF-8, só as de <, >, & e das aspas são necessárias; o resto pode ser digitado como texto comum.
Semântica: cada parte diz o que é
Semântica é usar o elemento que corresponde ao significado do conteúdo, e não ao visual que ele vai ter. Um título é um <h2> porque é um título de seção, não porque tem letra grande; quem decide o tamanho da letra é o CSS.

Os elementos de estrutura mais usados:
| Elemento | Significado |
|---|---|
header | o cabeçalho da página ou de uma seção |
nav | um bloco de links de navegação |
main | o conteúdo principal; um por página |
article | um conteúdo que faz sentido sozinho, como um post ou um produto |
section | uma seção temática, normalmente com um título |
aside | um conteúdo complementar, como um sumário ou links relacionados |
footer | o rodapé da página ou de uma seção |
figure e figcaption | uma imagem, diagrama ou trecho de código com a sua legenda |
time | uma data ou hora, com o valor legível por máquinas em datetime |
Os títulos, de <h1> a <h6>, formam o sumário do documento. Pule níveis só para cima: depois de um <h2> vem um <h3> ou outro <h2>, nunca um <h4> porque "o tamanho ficou melhor". Usuários de leitores de tela navegam muito pelos títulos, pulando de um para outro, e um sumário bagunçado é o equivalente a um livro com o índice embaralhado.
Outros pares merecem atenção. <strong> indica importância, e <em>, ênfase na fala; <b> e <i> só destacam visualmente, sem significado extra. Listas são <ul> ou <ol>, mesmo quando viram um menu horizontal. Tabelas são para dados tabulares, com <th> nos cabeçalhos e um <caption> descrevendo a tabela, e nunca para montar layout.
Link ou botão?
A dúvida mais comum tem uma regra simples: link leva a outro lugar, botão faz uma ação. "Ver pedido" é um link (<a href>), porque leva a outra página e pode ser aberto numa nova aba. "Salvar", "Excluir" e "Abrir menu" são botões (<button>).
O pior caminho é um <div> com um evento de clique fingindo ser botão. Ele não recebe foco pelo teclado, não responde a Enter nem a Espaço e é anunciado pelo leitor de tela como texto comum. Para corrigir isso, seria preciso acrescentar tabindex, role, tratamento de teclado e estados, ou seja, reconstruir à mão o que o <button> já entrega pronto.
Links por dentro
O <a> é o elemento que fez a web, e tem mais recursos do que o href:
- **Esquemas além do
https.**mailto:contato@exemplo.com.brabre o programa de e-mail, etel:+556699999999faz o celular discar. Num site de empresa, o telefone como link poupa o cliente de copiar o número. - **
download** pede ao navegador que baixe o arquivo em vez de abri-lo, e pode sugerir um nome:download="pedido-1042.pdf". Só vale para arquivos do próprio site. - **
rel** descreve a relação com o destino.noopenerenoreferrerprotegem a página e a privacidade ao abrir outro site;nofollow,sponsoredeugcdizem aos buscadores que o link não é um endosso, que é patrocinado ou que veio de um usuário, como num comentário. - **
hreflang** indica o idioma do destino, útil em sites com versões em vários idiomas.
O texto do link é tão importante quanto o destino. Leitores de tela oferecem uma lista com todos os links da página, fora de contexto, e uma lista com dez "clique aqui" não diz nada. "Baixe a tabela de preços (PDF, 2 MB)" diz o que acontece, para onde e quanto pesa.
Elementos de texto que quase ninguém usa
Além dos títulos, parágrafos e listas, o HTML tem elementos para situações que aparecem em qualquer texto de empresa, e usá-los dá significado de graça:
| Elemento | Para quê | Exemplo |
|---|---|---|
<abbr> | uma sigla, com a forma por extenso no title | <abbr title="Nota Fiscal eletrônica">NF-e</abbr> |
<dl>, <dt>, <dd> | pares de termo e descrição | uma ficha de produto: peso, dimensões, garantia |
<blockquote> e <q> | citação em bloco ou dentro da frase | o depoimento de um cliente, com cite apontando a fonte |
<code>, <pre>, <kbd> | código, texto pré-formatado, teclas | "pressione <kbd>Ctrl</kbd> + <kbd>F</kbd>" |
<mark> | um trecho destacado pela relevância | o termo buscado, realçado nos resultados |
<address> | o contato de quem publica a página ou o artigo | endereço e telefone no rodapé |
<data> | um valor com a versão para máquinas | <data value="2490">R$ 24,90</data> |
<del> e <ins> | texto removido e inserido | o preço antigo riscado ao lado do novo |
<small> | observações e letras miúdas | "Preços válidos até o fim do estoque." |
Dois avisos. O title do <abbr> não é lido por todos os leitores de tela nem aparece no celular; o mais seguro é escrever a forma por extenso na primeira vez, como este artigo faz. E o <hr>, que parece só uma linha, significa uma quebra de assunto dentro de uma seção; a linha é só a aparência padrão.
Acessibilidade: o HTML faz a maior parte do trabalho
Além do DOM, o navegador constrói uma segunda árvore, a árvore de acessibilidade, que é o que tecnologias assistivas como leitores de tela, ampliadores e controles por voz enxergam. Cada nó dela tem um papel (botão, link, caixa de texto, título), um nome (o texto que identifica o elemento) e um estado (marcado, expandido, obrigatório, inválido). Quase tudo isso vem direto do HTML: um <button>Salvar</button> já é "botão, Salvar", sem nenhuma linha extra.
Por isso a regra de ouro da acessibilidade na web é: use o elemento nativo certo. Algumas práticas cobrem a maioria dos problemas:
- Texto alternativo em imagens. O
altdescreve o que a imagem comunica. Imagem decorativa recebealt="", vazio, para ser ignorada. Imagem semaltfaz o leitor de tela ler o nome do arquivo. - Rótulo em todo campo. Um
<label>ligado ao campo porforeiddá nome ao campo e ainda aumenta a área de clique. Um placeholder não substitui o rótulo: ele some quando a pessoa começa a digitar. - Tudo pelo teclado. Todo controle precisa ser alcançável com Tab, na ordem visual, com o foco visível. Remover o contorno de foco no CSS sem colocar outro no lugar deixa quem navega pelo teclado às cegas.
- Um atalho para o conteúdo. Um link "Pular para o conteúdo" no começo da página, visível ao receber foco, poupa quem usa teclado de atravessar o menu inteiro em cada página.
- Contraste e tamanho. Não são HTML, mas fazem parte da mesma conta: texto com contraste suficiente e que possa ser ampliado sem quebrar o layout.
ARIA: quando o HTML não basta
O WAI-ARIA é um conjunto de atributos que acrescenta papéis, nomes e estados à árvore de acessibilidade, para componentes que o HTML não tem prontos, como abas ou um menu expansível. Exemplos: aria-expanded="true" num botão que abre um menu, aria-controls apontando para o menu, aria-current="page" no link da página atual, aria-describedby ligando um campo à sua mensagem de erro e aria-live numa região cujas atualizações devem ser anunciadas.
A primeira regra do ARIA, escrita pelo próprio W3C, é não usar ARIA quando existe um elemento nativo que faz o mesmo. O ARIA muda o que é anunciado, mas não acrescenta comportamento: um <div role="button"> continua sem foco e sem teclado. ARIA errado é pior que nenhum ARIA, porque promete ao usuário algo que a página não cumpre.
O nome acessível: de onde vem o que o leitor anuncia
Todo controle precisa de um nome, e o navegador o calcula por um algoritmo publicado pelo W3C, o Accessible Name and Description Computation. A ordem de consulta explica quase todos os casos estranhos:
- **
aria-labelledby** vem primeiro: o nome passa a ser o texto dos elementos apontados, na ordem em que aparecem no atributo, mesmo que esses elementos estejam escondidos. - **
aria-label**, um texto escrito direto no atributo. - O rótulo nativo da linguagem, como o
<label>de um campo ou oaltde uma imagem. - O conteúdo, para os elementos que recebem o nome do que têm dentro, como botões, links e títulos.
- **O
title**, a dica que aparece ao passar o mouse, só quando nenhum passo anterior produziu um nome.
Três consequências práticas saem dessa ordem. A primeira: um aria-label passa por cima do texto visível. Se o botão diz "Enviar" na tela e o aria-label diz "Concluir pedido", quem controla o computador pela voz e fala o que vê, "clicar em Enviar", pode não ser atendido. As WCAG tratam disso no critério 2.5.3, Rótulo no nome: o nome acessível precisa conter o texto que aparece na tela. A segunda: o title é o recurso mais fraco, e não aparece no celular nem para quem usa só o teclado. A terceira: um botão só com ícone não tem conteúdo de texto, e fica sem nome se ninguém der um a ele:
<!-- O nome vem do conteúdo: "Excluir pedido 1042" -->
<button type="button">
<svg aria-hidden="true" width="16" height="16"><use href="#icone-lixeira"></use></svg>
<span class="so-leitor">Excluir pedido 1042</span>
</button>
O aria-hidden="true" tira o desenho da árvore de acessibilidade, e o texto com a classe so-leitor, escondido da tela por CSS mas mantido na árvore, vira o nome do botão. Um aria-label="Excluir pedido 1042" no próprio botão teria o mesmo efeito. As ferramentas de desenvolvedor mostram, para cada elemento, o nome calculado e de qual passo ele veio, o que acaba com a adivinhação.
Foco e ordem de tabulação
A ordem em que o Tab percorre a página é a ordem do código, e os controles nativos, como links com href, botões e campos, já entram nela sem nenhum atributo. O tabindex muda isso, e cada valor tem um uso:
- **
tabindex="0"** põe na sequência um elemento que normalmente não receberia foco, na posição em que ele aparece no código. É o caso do contêiner de rolagem de uma tabela larga, que aparece mais adiante. - **
tabindex="-1"** deixa o elemento fora da sequência do Tab, mas permite que o JavaScript o foque comfocus(). Serve para levar o foco a uma mensagem de erro, a um diálogo que acabou de aparecer ou ao título de um conteúdo carregado sem recarregar a página. - Valores positivos passam o elemento à frente de todos os outros, na ordem dos números. A MDN recomenda usar só
0e-1: uma ordem de foco que não segue a leitura confunde quem navega pelo teclado, e manter os números certos numa página que cresce é inviável.
Dois cuidados completam o quadro. O tabindex não transforma um <div> em controle: ele passa a receber foco, mas continua sem papel, sem nome de botão e sem resposta ao Enter, como no exemplo do botão de mentira. E, em aplicações que trocam o conteúdo da tela sem recarregar a página, o foco precisa ser levado ao conteúdo novo, normalmente ao título dele, com tabindex="-1". Sem isso, quem usa leitor de tela continua parado no link que acabou de acionar, sem notícia de que a tela mudou.
A acessibilidade não é só boa prática. As diretrizes internacionais de referência são as WCAG, do W3C, hoje na versão 2.2. No Brasil, a Lei Brasileira de Inclusão (Lei 13.146/2015), no artigo 63, torna obrigatória a acessibilidade nos sites mantidos por empresas com sede ou representação comercial no país, conforme as melhores práticas adotadas internacionalmente.
Testando na prática, em quinze minutos
Acessibilidade se confere usando a página do jeito que outras pessoas usam. Um roteiro curto já encontra a maior parte dos problemas:
- Largue o mouse. Percorra a página com Tab e Shift+Tab. O foco precisa estar sempre visível, seguir a ordem da leitura e alcançar todos os controles. Abra menus e janelas com Enter ou Espaço, feche com Esc e confira se o foco volta para onde estava.
- Amplie para 200% com Ctrl e a tecla de mais. Nada pode ficar cortado ou sobreposto, e o texto não deve exigir rolagem para os lados para ser lido.
- Ouça a página. O NVDA, gratuito, e o Narrador, que já vem no Windows, leem a página em voz alta; no Mac e no iPhone, o VoiceOver; no Android, o TalkBack. Navegue pelos títulos e pelos campos do formulário e pergunte: cada coisa tem um nome que faz sentido fora do contexto visual?
- Olhe a árvore de acessibilidade. As ferramentas de desenvolvedor do Chrome e do Firefox mostram, para cada elemento, o papel, o nome e o estado que o leitor de tela recebe.
- Rode um verificador automático, como o Lighthouse, que vem no Chrome, ou o axe. Eles acham contraste insuficiente, imagens sem
alte campos sem rótulo em segundos, mas só uma parte dos problemas pode ser detectada por máquina: se o texto alternativo descreve bem a imagem, só uma pessoa sabe dizer.
Tabelas de dados bem feitas
Relatórios de vendas, extratos, listas de títulos a receber: boa parte das telas de um sistema de gestão é uma tabela. Uma tabela bem marcada permite que o leitor de tela anuncie, para cada célula, a linha e a coluna a que ela pertence. Uma tabela mal marcada vira uma sequência de números soltos.

<table>
<caption>Vendas por loja, setembro de 2026</caption>
<thead>
<tr>
<th scope="col">Loja</th>
<th scope="col">Pedidos</th>
<th scope="col">Faturamento</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Centro</th>
<td>1.204</td>
<td>R$ 312.480,00</td>
</tr>
<tr>
<th scope="row">Bairro Novo</th>
<td>868</td>
<td>R$ 198.920,00</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row">Total</th>
<td>2.072</td>
<td>R$ 511.400,00</td>
</tr>
</tfoot>
</table>
O <caption> é o título da tabela, anunciado quando o leitor de tela chega a ela. O <th> marca as células de cabeçalho, e o scope diz se ele vale para a coluna (col) ou para a linha (row). Com isso, ao chegar no valor R$ 198.920,00, o leitor anuncia algo como "Bairro Novo, Faturamento, R$ 198.920,00". O <thead>, o <tbody> e o <tfoot> separam cabeçalho, dados e totais; na impressão de uma tabela longa, os navegadores podem repetir o cabeçalho em cada página. Em tabelas com cabeçalhos em vários níveis, os atributos id e headers ligam cada célula explicitamente aos seus cabeçalhos, mas, quando a tabela fica complexa demais para isso, dividi-la em duas costuma ser a melhor saída.
Três cuidados de apresentação completam o trabalho. Números alinhados à direita, com a propriedade CSS font-variant-numeric: tabular-nums, que dá a todos os algarismos a mesma largura, ficam fáceis de comparar. Em telas estreitas, a tabela vai dentro de um contêiner com rolagem horizontal, em vez de ter as colunas espremidas; se esse contêiner puder receber foco, quem usa teclado também consegue rolá-lo. E mudar o display de uma tabela no CSS para reorganizá-la no celular pode, em alguns navegadores, apagar a semântica dela na árvore de acessibilidade.
Formulários: onde o HTML mais trabalha sozinho
Formulários são a parte do HTML que mais entrega pronto, e a mais reinventada sem necessidade com JavaScript.

Com os atributos certos, o navegador faz muito sozinho:
- **
type** escolhe o controle e o teclado no celular:email,tel,url,number,date,search,password,checkbox,radio,file. - **
inputmode** ajusta o teclado sem mudar a validação;inputmode="numeric"num campo de CNPJ mostra o teclado numérico. - **
autocomplete** permite o preenchimento automático e o gerenciador de senhas:name,email,tel,postal-code,current-password,new-password,one-time-code. - **
required**, **minlength**, **maxlength**, **min**, **max** e **pattern** ativam a validação nativa, que impede o envio e mostra uma mensagem antes de qualquer requisição. - **
<fieldset>** e **<legend>** agrupam campos relacionados, como as opções de um grupo de botões de rádio, e dão um nome ao grupo.
A validação do navegador é uma conveniência para quem preenche, e não uma proteção. Qualquer pessoa pode desligá-la ou enviar a requisição sem passar pela página. O servidor precisa validar tudo de novo, sempre. E as regras nativas têm limites: para o type="email", por exemplo, ana@exemplo é válido, porque a especificação não exige um ponto no domínio.
O que o envio gera
Um formulário com method="get" envia os campos na URL, como em /busca?q=caf%C3%A9. É o certo para buscas e filtros, que não alteram nada e podem ser compartilhados e guardados nos favoritos. Com method="post", os campos vão no corpo da requisição, codificados como application/x-www-form-urlencoded, ou como multipart/form-data quando há envio de arquivos (enctype="multipart/form-data").
Dois detalhes evitam bugs clássicos. Um <button> dentro de um formulário é, por padrão, do tipo submit: um botão "Cancelar" sem type="button" envia o formulário. E campos sem o atributo name não são enviados, mesmo que estejam preenchidos na tela.
Passo a passo: um cadastro de cliente
Juntando as peças, um cadastro de cliente para uma distribuidora fica assim:
<form action="/clientes" method="post">
<fieldset>
<legend>Dados da empresa</legend>
<label for="razao">Razão social</label>
<input id="razao" name="razao" autocomplete="organization" required>
<label for="cnpj">CNPJ</label>
<input id="cnpj" name="cnpj" inputmode="numeric" required
pattern="\d{2}\.?\d{3}\.?\d{3}/?\d{4}-?\d{2}"
aria-describedby="cnpj-dica">
<p id="cnpj-dica">Só os números ou no formato 00.000.000/0000-00.</p>
</fieldset>
<fieldset>
<legend>Contato do financeiro</legend>
<label for="email">E-mail</label>
<input id="email" name="email" type="email" autocomplete="email" required>
<label for="tel">Telefone</label>
<input id="tel" name="tel" type="tel" autocomplete="tel">
<label for="cep">CEP</label>
<input id="cep" name="cep" inputmode="numeric" autocomplete="postal-code"
pattern="\d{5}-?\d{3}">
</fieldset>
<button type="submit">Cadastrar cliente</button>
</form>
O que cada decisão entrega, sem uma linha de JavaScript:
- Os dois
<fieldset>com<legend>dividem o formulário em blocos com nome; o leitor de tela anuncia "Contato do financeiro" ao entrar no segundo. - Cada
<label>comfordá nome ao campo e torna o rótulo clicável. - O CNPJ e o CEP usam
inputmode="numeric", e nãotype="number": o teclado numérico aparece no celular, mas o campo continua aceitando pontos, barras e zeros à esquerda, que um campo numérico descartaria. Números que não servem para contas, como CNPJ, CEP e telefone, são texto. - O
patternconfere o formato antes do envio, e oaria-describedbyfaz o leitor de tela ler a dica junto com o rótulo. - Os valores de
autocompletepermitem ao navegador preencher razão social, e-mail, telefone e CEP com um toque. - O botão tem
type="submit"explícito e um texto que diz o que acontece.
O que o HTML não faz é conferir os dígitos verificadores do CNPJ: isso é regra de negócio, e fica no servidor, que valida tudo de novo de qualquer forma.
Mensagens de erro que todos conseguem ler
As mensagens nativas de validação aparecem no idioma do navegador e somem em poucos segundos, o que basta para formulários simples. Para ter controle sobre o texto e a aparência, a página pode assumir a exibição dos erros:
- O atributo
novalidateno<form>desliga os balões nativos, mas mantém a validação disponível para o JavaScript pela Constraint Validation API:campo.validity.valueMissingdiz se o campo obrigatório está vazio,campo.validity.patternMismatchdiz se o formato não bate,form.checkValidity()confere tudo ecampo.setCustomValidity('mensagem')define um erro próprio. - Cada mensagem fica num elemento ligado ao campo por
aria-describedby, e o campo com erro recebearia-invalid="true". Assim, o leitor de tela anuncia o erro ao chegar no campo. - Ao tentar enviar, o foco vai para o primeiro campo com erro. Em formulários longos, um resumo dos erros no topo, com links para cada campo, ajuda quem está longe do problema.
- O erro diz como corrigir: "Informe o CNPJ com 14 dígitos" ajuda; "Campo inválido" não.
- No CSS, a pseudoclasse
:user-invalidsó se aplica depois que a pessoa interagiu com o campo, ao contrário de:invalid, que pinta de vermelho um formulário que ninguém começou a preencher.
Dos bytes aos pixels: como o navegador desenha a página
Entre receber o HTML e mostrar a página, o navegador percorre uma sequência de etapas, e entender essa sequência explica a maior parte das dicas de desempenho.

- Leitura do HTML. Os bytes viram caracteres, os caracteres viram tokens (tags, atributos, textos) e os tokens viram os nós do DOM. Enquanto isso, um leitor auxiliar (preload scanner) olha adiante no arquivo e já começa a baixar CSS, scripts e imagens que encontra.
- Leitura do CSS. As folhas de estilo viram o CSSOM, com a cascata resolvida: para cada elemento, qual regra vence.
- Render tree. DOM e CSSOM se combinam numa árvore só com o que aparece, já com os estilos calculados. Um elemento com
display: nonenem entra. - Layout. O navegador calcula a posição e o tamanho de cada caixa na tela.
- Pintura. Cada caixa vira pixels: cores, textos, bordas, sombras e imagens.
- Composição. As camadas pintadas são combinadas na ordem certa e enviadas para a tela, muitas vezes com ajuda da placa de vídeo.
Duas regras saem daí. O CSS bloqueia a pintura: o navegador não mostra a página sem o CSSOM, para não exibi-la sem estilo e depois "pular". Por isso o CSS crítico deve ser pequeno e chegar cedo. O script clássico bloqueia a leitura: um <script> comum pode alterar o documento enquanto ele é lido, então o parser para, espera o script baixar e executar, e só então continua.
async, defer e módulos
Os atributos de carregamento existem para evitar esse bloqueio:
| Forma | Baixa | Executa | Ordem garantida |
|---|---|---|---|
<script> | parando a leitura | logo após baixar | sim |
<script async> | em paralelo | assim que baixa, interrompendo a leitura | não |
<script defer> | em paralelo | depois de ler todo o HTML | sim |
<script type="module"> | em paralelo | depois de ler todo o HTML, como o defer | sim |
Na prática: scripts da aplicação vão com defer ou como módulos; scripts independentes, como os de métricas, podem ir com async; e o <script> comum no <head> sem atributos deve ser exceção.
Depois da primeira pintura, cada mudança que altera tamanhos ou posições obriga o navegador a refazer o layout de parte da página, e cada mudança visual, a pintar de novo. Imagens sem dimensões são uma causa clássica: quando chegam, empurram o texto para baixo. Declarar width e height na imagem reserva o espaço desde o início e evita esse salto, medido pelo indicador CLS das Core Web Vitals do Google.
O desempenho começa no head
O <head> pode avisar o navegador, antes de ele descobrir sozinho, do que vai ser preciso. São as dicas de recursos, todas no elemento <link>:
| Dica | O que faz | Quando usar |
|---|---|---|
rel="preconnect" | abre cedo a conexão com outro domínio, com DNS, TCP e TLS | fontes ou API em outro endereço, usados logo no início |
rel="dns-prefetch" | só resolve o nome do domínio | domínios usados mais tarde na página |
rel="preload" | baixa já, com prioridade, um recurso desta página | a fonte do título ou a imagem principal, descobertas tarde pelo CSS |
rel="modulepreload" | baixa e prepara um módulo JavaScript | módulos importados por outros módulos |
rel="prefetch" | baixa com baixa prioridade algo da próxima página | o passo seguinte de um fluxo previsível, como o carrinho |
As dicas custam banda e disputam a mesma conexão, então mais não é melhor: um preload de algo que não será usado nos primeiros segundos atrasa o que importa, e o próprio navegador avisa no console quando um recurso pré-carregado não foi usado. Uma fonte pré-carregada precisa do atributo crossorigin, mesmo vinda do próprio site, ou será baixada duas vezes.
O tamanho do DOM também pesa. Cada elemento custa memória e entra nos cálculos de estilo e de layout, e o Lighthouse aponta páginas com mais de cerca de 1.400 elementos. Tabelas com milhares de linhas, listas infinitas e camadas de <div> que só existem para posicionar coisas são as causas habituais; paginar, carregar sob demanda e achatar a estrutura resolvem a maioria dos casos.
Primeiro o HTML, depois o resto
Uma consequência de tudo isso é uma estratégia antiga e ainda valiosa, o aprimoramento progressivo: a página funciona primeiro com HTML puro, o CSS a deixa bonita e o JavaScript acrescenta conforto. Um formulário de busca que envia por GET funciona sem script; com script, ganha sugestões enquanto a pessoa digita. Um link "Ver mais" leva à página seguinte; com script, carrega os itens no lugar. Quando algo falha, e na web algo sempre falha para alguém, por uma rede instável, uma extensão do navegador ou um arquivo que não carregou, a pessoa ainda consegue concluir o que veio fazer.
Imagens e mídia
Imagens costumam ser a maior parte dos bytes de uma página. O HTML oferece tudo o que é preciso para entregá-las bem:
<img
src="/fotos/gondola-800.jpg"
srcset="/fotos/gondola-400.jpg 400w, /fotos/gondola-800.jpg 800w, /fotos/gondola-1600.jpg 1600w"
sizes="(max-width: 700px) 100vw, 700px"
width="800" height="450"
alt="Gôndola de supermercado com etiquetas de preço eletrônicas"
loading="lazy" decoding="async">
O srcset lista versões da mesma imagem em larguras diferentes, e o sizes diz quanto espaço ela ocupa na tela; com isso, o navegador escolhe o arquivo certo para cada aparelho, sem baixar uma foto de 1.600 pixels num celular. O loading="lazy" adia imagens fora da tela, e o decoding="async" evita travar a pintura enquanto a imagem é decodificada. A imagem principal do topo da página, ao contrário, não deve ser preguiçosa; ela pode até receber fetchpriority="high". O elemento <picture> vai além e permite oferecer formatos diferentes, como AVIF e WebP, com um JPEG de reserva:
<picture>
<source type="image/avif" srcset="/fotos/fachada.avif">
<source type="image/webp" srcset="/fotos/fachada.webp">
<img src="/fotos/fachada.jpg" width="1200" height="675"
alt="Fachada da loja Centro, com a entrada de clientes e o estacionamento">
</picture>
O navegador percorre os <source> na ordem e usa o primeiro formato que sabe exibir; quem não conhece nenhum fica com o <img>, que também é quem carrega o alt e as dimensões. Os formatos modernos costumam gerar arquivos bem menores que o JPEG com a mesma qualidade visual, o que se traduz em páginas mais rápidas em redes móveis.
Vídeo e áudio têm elementos próprios, <video> e <audio>, com controles nativos e legendas pelo elemento <track>, que aponta para um arquivo de legendas no formato WebVTT. Legendas atendem quem não ouve e também quem assiste a um vídeo de treinamento no escritório, sem fone. Um vídeo que começa sozinho precisa estar sem som (muted), ou os navegadores bloqueiam a reprodução automática.
HTML para buscadores e redes sociais
Os robôs de busca e os geradores de prévias das redes sociais leem o HTML, e uma parte do que eles mostram vem de marcações específicas:
- O
<title>e adescriptionaparecem no resultado de busca. - As metas Open Graph (
og:title,og:description,og:image) definem o cartão que aparece quando o link é compartilhado no WhatsApp, no LinkedIn ou no Facebook. - Os dados estruturados em JSON-LD, dentro de um
<script type="application/ld+json">, descrevem o conteúdo com o vocabulário do schema.org: este é um artigo, com este autor e esta data; esta é uma empresa, com este endereço. Eles alimentam os resultados enriquecidos do Google. - O
canonicalevita conteúdo duplicado, e a metarobotscomnoindextira do índice páginas que não devem aparecer, como as de conta e de busca interna.
Na prática, o <head> de um artigo fica parecido com este:
<meta property="og:type" content="article">
<meta property="og:title" content="Como funciona o boleto com Pix">
<meta property="og:description" content="O que muda para o financeiro com o QR Code no boleto.">
<meta property="og:image" content="https://exemplo.com.br/capas/boleto-pix.jpg">
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BlogPosting",
"headline":"Como funciona o boleto com Pix",
"datePublished":"2026-10-10T06:00:00-04:00",
"publisher":{"@type":"Organization","name":"Exemplo Ltda"}}
</script>
A imagem do og:image deve ter endereço completo, com https://, e um tamanho próximo de 1200 por 630 pixels, a proporção que as redes usam no cartão. Como os robôs das redes guardam a prévia em cache, corrigir a imagem depois de compartilhar o link nem sempre muda o cartão de imediato; as ferramentas de depuração de cada rede pedem uma nova leitura.
Há também uma questão de arquitetura. Uma aplicação que entrega um HTML vazio e monta tudo com JavaScript obriga o buscador a executar o script para ver o conteúdo, e muitos robôs de redes sociais nem tentam. Renderizar no servidor ou pré-renderizar as páginas no build entrega o HTML completo desde a primeira resposta, o que é melhor para os robôs e também para o usuário, que vê o conteúdo antes de o JavaScript terminar de carregar.
Segurança: o HTML como superfície de ataque
O ataque mais comum na web, o XSS (cross-site scripting), acontece quando um texto vindo de um usuário vai parar no HTML sem tratamento e o navegador o interpreta como marcação. Um comentário com <script> ou uma imagem com onerror passa a executar código no navegador de quem abre a página, com acesso à sessão.
As defesas se somam:
- Escape tudo o que vem de fora antes de colocar no HTML:
<vira<,&vira&, aspas viram entidades. Frameworks como o React fazem isso por padrão; o perigo está nas saídas de emergência, comoinnerHTMLedangerouslySetInnerHTML. - **Prefira
textContent** ainnerHTMLao inserir texto com JavaScript. - Aplique uma Content Security Policy, o cabeçalho que diz de onde a página pode carregar scripts. Com
script-src 'self', um script injetado no meio do HTML simplesmente não roda. - Isole conteúdo de terceiros com o atributo
sandboxem<iframe>. - Links para outros sites abertos em nova aba devem usar
rel="noopener", para que a página aberta não controle a sua; os navegadores atuais já aplicam esse comportamento aotarget="_blank", mas o atributo explícito não custa nada.
O escape certo depende de onde o texto entra, e esse é o ponto que mais engana. Dentro do conteúdo de um elemento, basta escapar < e &. Dentro de um atributo, as aspas também, e o atributo precisa estar entre aspas. Dentro de um <script>, como num bloco de JSON-LD, o escape de HTML não serve, porque o parser lê aquele trecho como texto cru: um título de post com </script> encerra o script no meio, e o que vem depois vira HTML. A solução é trocar o < por \u003c dentro do JSON, que continua valendo o mesmo caractere para o JavaScript e nunca forma uma tag. E dentro de um href, nenhum escape impede um endereço javascript:; é preciso aceitar só esquemas conhecidos, como https: e mailto:.
Há também o ataque inverso, em que o seu site é a vítima dentro de outro: o clickjacking. Um site malicioso abre a sua página num <iframe> invisível, por cima de um botão chamativo, e a pessoa clica, sem saber, num botão da sua página, como "confirmar" ou "excluir". A defesa é proibir que as páginas sejam emolduradas por outros sites, com a diretiva frame-ancestors da Content Security Policy.
iframes e arquivos de terceiros
Mapas, vídeos, gráficos, botões de pagamento: muitas páginas incorporam conteúdo de outros sites, e cada um é código alheio rodando dentro da página ou ao lado dela. O HTML tem atributos para limitar o que esse código pode fazer.
No <iframe>, o **sandbox** vazio aplica todas as restrições: o conteúdo não roda scripts, não envia formulários, não abre janelas e é tratado como vindo de uma origem especial, que sempre falha nas regras de mesma origem. Cada palavra acrescentada libera uma permissão, como allow-scripts, allow-forms, allow-popups e allow-same-origin. A combinação de allow-scripts com allow-same-origin num conteúdo da mesma origem da página é a que a MDN desaconselha com mais força: o documento incorporado consegue remover o próprio sandbox, e a proteção deixa de existir.
Outros atributos completam o quadro. O **allow** controla o acesso a recursos como câmera e microfone, como uma restrição a mais por cima do cabeçalho Permissions-Policy. O **referrerpolicy** define quanto do endereço da sua página vai para o outro site. O **loading="lazy"**, que funciona em iframes em todos os principais navegadores desde dezembro de 2023, adia o carregamento até o quadro chegar perto da área visível. E o **title** dá nome ao quadro, para que quem usa leitor de tela saiba o que há dentro sem precisar entrar nele.
<iframe src="https://mapas.exemplo.com.br/loja-centro"
title="Mapa com a localização da loja Centro"
width="600" height="400" loading="lazy"
referrerpolicy="no-referrer"
sandbox="allow-scripts"></iframe>
Para scripts e folhas de estilo vindos de outro endereço, como uma biblioteca servida por uma CDN, existe a integridade de sub-recursos (SRI). O atributo integrity traz o resumo criptográfico esperado do arquivo, com SHA-256, SHA-384 ou SHA-512. O navegador calcula o resumo do que baixou e, se não bater, recusa o arquivo: o script não roda e o estilo não se aplica. Se a CDN for invadida e o arquivo trocado, a página deixa de carregá-lo em vez de executar o código de outra pessoa. Para arquivos de outra origem, a verificação exige CORS, com o atributo crossorigin="anonymous" na tag e o cabeçalho de permissão no servidor; sem isso, o carregamento com integrity falha sempre.
<script src="https://cdn.exemplo.com.br/graficos@4.2.1/graficos.min.js"
integrity="sha384-RESUMO_EM_BASE64_DO_ARQUIVO"
crossorigin="anonymous"></script>
O resumo sai do próprio arquivo, por exemplo com openssl dgst -sha384 -binary graficos.min.js | openssl base64 -A. E a versão no endereço precisa ser fixa: um endereço que sempre aponta para "a versão mais recente" muda de conteúdo a cada atualização da biblioteca, e a verificação passa a recusar o arquivo novo.
Recursos nativos que dispensam JavaScript
O HTML ganhou, nos últimos anos, elementos que substituem componentes que antes exigiam bibliotecas inteiras:
<details>e<summary>criam um bloco que abre e fecha, acessível e sem nenhum script, perfeito para perguntas frequentes.<dialog>cria uma janela modal com foco preso dentro dela, fechamento pela tecla Esc e o fundo inerte.- O atributo
popovertransforma qualquer elemento num balão que abre por um botão e fecha ao clicar fora. <datalist>oferece sugestões para um campo de texto.- O atributo
inertdesativa uma região inteira da página, inclusive para o teclado e o leitor de tela.
Cada recurso nativo usado no lugar de um componente feito à mão é menos código para manter, menos JavaScript para baixar e, quase sempre, mais acessibilidade.
Diálogos por dentro
O <dialog> merece um olhar mais de perto, porque substitui um dos componentes mais difíceis de acertar à mão. Ele tem dois modos. Aberto com showModal(), vira um modal de verdade: vai para a camada superior da página, acima de qualquer outro elemento, ganha um fundo que o CSS estiliza pelo pseudoelemento ::backdrop, torna inerte tudo o que está fora dele e fecha com Esc. Aberto com show(), aparece sem bloquear o resto da página. A MDN recomenda usar esses dois métodos, em vez de acrescentar à mão o atributo open.
<button type="button" commandfor="excluir" command="show-modal">Excluir pedido</button>
<dialog id="excluir" aria-labelledby="excluir-titulo" closedby="any">
<h2 id="excluir-titulo">Excluir o pedido 1042?</h2>
<p>Os itens voltam para o estoque. Esta ação não pode ser desfeita.</p>
<form method="dialog">
<button value="cancelar" autofocus>Cancelar</button>
<button value="excluir">Excluir</button>
</form>
</dialog>
Cada peça tem um papel:
- O formulário com
method="dialog"fecha o diálogo ao ser enviado, sem mandar nada ao servidor, e oreturnValuedo diálogo recebe ovaluedo botão usado. O script lê esse valor no eventoclosepara saber a resposta. - O
autofocusno "Cancelar" põe o foco na opção segura assim que o diálogo abre. Sem ele, o foco vai para o primeiro elemento focável de dentro do diálogo. - O
aria-labelledbydá ao diálogo o nome do título, que o leitor de tela anuncia na abertura. - O
closedby="any"permite fechar também com um clique fora. Sem ele, um diálogo modal fecha pelo Esc e pelos botões. É um atributo mais novo, e vale conferir o suporte dos navegadores antes de depender dele. - O botão que abre o diálogo usa os atributos
commandforecommand, descritos a seguir, e dispensa o JavaScript.
Um último aviso da MDN: o <dialog> não deve receber tabindex. Ele não é um controle; quem recebe o foco são os elementos de dentro.
O que chegou ao HTML nos últimos anos
O HTML vivo cresce aos poucos, e alguns acréscimos recentes já funcionam em todos os principais navegadores, segundo o selo Baseline, mantido pelo web.dev e exibido na MDN:
- **
<search>**, disponível desde outubro de 2023, marca a região de busca ou de filtros da página. Ele cria um marco de busca na árvore de acessibilidade, o que dispensa o antigorole="search"no formulário. Os resultados não vão dentro dele: ficam no conteúdo principal. - **O atributo
inert**, Baseline desde abril de 2023, é o mesmo mecanismo que o diálogo modal usa por dentro, à disposição de qualquer região da página, como um painel lateral aberto por cima do conteúdo. - Popovers, Baseline desde janeiro de 2025. O selo chegou a ser anunciado em abril de 2024, mas um defeito sério no suporte de um dos motores de navegador passou despercebido, e o web.dev corrigiu a data. O atributo aceita três valores:
auto, o padrão, que fecha com Esc ou com um clique fora e fecha os outros popoversautoabertos;manual, que só fecha por um botão ou por script; ehint, para dicas que aparecem ao passar o mouse ou ao receber foco, sem fechar os popoversauto. O popover aberto também vai para a camada superior, livre dooverflowe do posicionamento dos ancestrais. - Acordeões exclusivos, Baseline desde setembro de 2024: vários
<details>com o mesmo atributonameformam um grupo em que só um fica aberto por vez. Abrir um fecha os outros, sem script, e os elementos nem precisam estar lado a lado no código. - Comandos de botão, Baseline desde dezembro de 2025: os atributos
commandforecommanddeixam um<button>abrir um diálogo como modal (show-modal), fechá-lo (close) ou mostrar, esconder e alternar um popover, sem JavaScript. Valores próprios, começados por dois hífens, como--girar, disparam um eventocommandno elemento controlado, para a página tratar como quiser. - **
hidden="until-found"** esconde um conteúdo sem tirá-lo do alcance da busca do navegador (Ctrl+F) nem dos links com#. Quando a busca encontra o texto, o navegador dispara o eventobeforematch, tira ohiddene rola até o trecho. É o jeito de fazer seções recolhidas que continuam aparecendo na busca dentro da página; como o suporte é mais recente, vale conferir a tabela de compatibilidade da MDN antes de depender dele.
<details name="faq" open>
<summary>Qual é o prazo de entrega?</summary>
<p>Até dois dias úteis para pedidos feitos até as 14h.</p>
</details>
<details name="faq">
<summary>Posso trocar um produto?</summary>
<p>Sim, em até sete dias após o recebimento.</p>
</details>
O exemplo é uma seção de perguntas frequentes inteira, com teclado, leitor de tela e o comportamento de acordeão, em oito linhas de HTML e nenhuma de script. Um navegador antigo, que não conhece o name, mostra os blocos independentes, e a página continua funcionando: é o aprimoramento progressivo descrito acima, aplicado a um componente.
Elementos próprios: os Web Components
Quando nenhum elemento nativo serve, o próprio HTML permite criar novos. Os Web Components são um conjunto de recursos do navegador para isso. Os elementos personalizados são registrados com customElements.define('cartao-pedido', CartaoPedido) e usados como qualquer tag, <cartao-pedido numero="1042">; o nome precisa ter um hífen, para nunca colidir com um elemento que o HTML venha a criar. O Shadow DOM dá ao componente uma árvore própria, isolada, em que o CSS de fora não entra e o de dentro não vaza. O <template> guarda o molde, e o <slot> marca onde entra o conteúdo que quem usa o componente coloca entre as tags.
A vantagem é a independência: um Web Component funciona em qualquer página, com ou sem framework, o que faz deles uma boa escolha para peças compartilhadas entre sistemas diferentes. O cuidado é o mesmo de sempre: um elemento novo não tem semântica nenhuma, e a acessibilidade, do foco ao papel anunciado, é responsabilidade de quem o escreve. Sempre que possível, o componente deve envolver um elemento nativo, como um <button> de verdade, em vez de imitá-lo.
Por muito tempo, o Shadow DOM só podia ser criado por JavaScript, com attachShadow(), e a página precisava esperar o script para mostrar o componente montado. O Shadow DOM declarativo, Baseline desde fevereiro de 2024, permite escrevê-lo direto no HTML, com um <template shadowrootmode="open"> dentro do elemento. O parser troca esse <template> por uma raiz de sombra presa ao elemento pai já durante a leitura, e um servidor consegue entregar componentes com estilos isolados prontos na primeira resposta:
<cartao-pedido>
<template shadowrootmode="open">
<style>:host { display: block; border: 1px solid #ccc; padding: 1rem; }</style>
<slot></slot>
</template>
<p>Pedido 1042, aberto</p>
</cartao-pedido>
O parágrafo fica no DOM comum e aparece no lugar do <slot>; o estilo vale só dentro do componente. O valor closed esconde a raiz de sombra do JavaScript da página, e só o primeiro <template shadowrootmode> de cada elemento vira raiz. A conversão acontece apenas na leitura do HTML: mudar o atributo depois, por script, não cria raiz nenhuma.
HTML em e-mails: outra web, com outras regras
O e-mail de uma newsletter, de um aviso de cobrança ou de uma redefinição de senha também é HTML, mas lido por programas muito diferentes de um navegador. O Gmail, o Outlook, o Apple Mail e os aplicativos de celular aceitam, cada um, um subconjunto próprio de HTML e CSS. O Outlook clássico para Windows, por exemplo, desenha as mensagens com o motor do Word, que ignora boa parte do CSS moderno. Daí vem um conjunto de práticas que parece uma volta aos anos 2000, e é de propósito:
- Layout com tabelas. Flexbox e grid não funcionam em todos os leitores; tabelas, sim. Elas recebem
role="presentation", para que o leitor de tela não as anuncie como tabelas de dados. - Largura fixa em torno de 600 pixels, com uma regra
@mediaque a transforma em 100% no celular, nos leitores que aceitam. - Estilos no próprio elemento, com o atributo
style, porque alguns leitores descartam o<style>do cabeçalho. - Nada de JavaScript. Nenhum leitor de e-mail executa scripts, e formulários raramente funcionam.
- Texto que não depende de imagem. Muitos leitores bloqueiam imagens até a pessoa permitir, então o
alte o texto ao redor precisam carregar a mensagem sozinhos. - O texto de pré-visualização (preheader): um trecho escondido no início do corpo, que a caixa de entrada mostra ao lado do assunto.
- **
lange<title>** continuam valendo: os leitores de tela também leem e-mails.
E vale a mesma regra de segurança das páginas: todo texto variável, como o nome do cliente ou o assunto, é escapado antes de entrar no HTML.
Erros comuns
- **Esquecer o
<!doctype html>** e cair no modo de compatibilidade. - **Omitir o
lang**, ou deixar oende um modelo pronto numa página em português. - **
<div>e<span>para tudo**, inclusive para botões e links. - **Imagens sem
alt**, ou comalt="imagem". - **Campos sem
<label>**, só com placeholder. - Títulos escolhidos pelo tamanho da letra, pulando níveis.
- Tabelas para layout e layout para dados tabulares.
- Remover o contorno de foco sem oferecer outro.
- **Botão sem
type** dentro de formulário, enviando o formulário sem querer. - Confiar na validação do navegador como proteção.
- **Inserir texto de usuário com
innerHTML.** - **Imagens sem
widtheheight**, fazendo a página pular quando carregam. - Página montada só com JavaScript quando o conteúdo precisa ser encontrado no Google ou compartilhado nas redes.
- **
type="number"para CNPJ, CEP ou telefone**, perdendo zeros à esquerda e a formatação. - **Tabelas sem
<th>nem<caption>**, que viram números soltos para o leitor de tela. - Links "clique aqui", que não dizem nada fora do contexto.
- Seletores CSS que esperam a árvore escrita, e não a do parser, como
table > tr. - **
tabindexpositivo** para "consertar" a ordem do foco, em vez de reorganizar o código. - Um botão dentro de um link, ou um link dentro de um botão.
- **
aria-labeldiferente do texto visível**, que quebra o controle por voz. - **
<iframe>semtitle** e semsandboxquando o conteúdo é de terceiros.
Perguntas frequentes
Preciso fechar todas as tags? A especificação permite omitir alguns fechamentos, como </p> e </li>, e até <html>, <head> e <body>. Permitir não quer dizer recomendar: fechar tudo deixa a estrutura explícita para quem lê e evita surpresas com os fechamentos automáticos.
HTML é uma linguagem de programação? Não. Ele não tem lógica, laços nem variáveis: descreve conteúdo. Isso não o torna menos importante; é a camada da qual dependem o CSS, o JavaScript, a acessibilidade e a busca.
**Usar <div> é errado?** Não. O <div> é o elemento certo para agrupar conteúdo sem significado próprio, por exemplo para fins de layout. O problema é usá-lo no lugar de um elemento que tem significado, como um botão, uma lista ou um título.
**Quantos <h1> uma página pode ter?** A especificação não proíbe vários, mas leitores de tela e buscadores funcionam melhor com um só, o título da página, e os demais níveis abaixo dele.
Renderizar no servidor ou no navegador? Depende do que a página faz. Conteúdo público, que precisa ser encontrado e compartilhado, como um blog, uma loja ou um site institucional, ganha muito com o HTML pronto do servidor. Painéis internos, atrás de um login e com muita interação, como o de um CRM, podem ser montados no navegador sem perder nada, porque nenhum buscador precisa vê-los.
Como sei se o meu HTML está correto? O validador do W3C aponta erros de sintaxe e de aninhamento. Para acessibilidade, o roteiro de testes descrito acima, com teclado, zoom, leitor de tela e um verificador automático, cobre o essencial.
Como usamos HTML na Gothor
As páginas públicas da Gothor, como este blog e o site, são pensadas para chegar prontas ao navegador: HTML semântico, legível por pessoas, por leitores de tela e por mecanismos de busca, com o JavaScript só como complemento. Se um script não carregar, o conteúdo continua lá. Formulários com rótulos de verdade, imagens com descrição e navegação pelo teclado fazem parte do mesmo cuidado.
Resumo
- O HTML descreve o significado do conteúdo; o CSS cuida da aparência e o JavaScript, do comportamento.
<!doctype html>,lang,charset, viewport e<title>são o mínimo de todo documento.- O navegador transforma o HTML no DOM com um parser tolerante e padronizado; erros não aparecem, só mudam a árvore, que pode ganhar elementos implícitos como o
<tbody>. - Semântica é escolher o elemento pelo significado: regiões, títulos em ordem, listas, tabelas para dados, link para navegar e botão para agir.
- A árvore de acessibilidade vem quase toda do HTML. Use o elemento nativo antes de qualquer ARIA, e teste com teclado, zoom e leitor de tela.
- Tabelas de dados levam
<caption>,<th>escope, para que cada valor seja anunciado com a linha e a coluna. - Formulários ganham teclado certo, preenchimento automático e validação com atributos, mas o servidor valida de novo; erros ficam ligados aos campos.
- CSS bloqueia a pintura e script clássico bloqueia a leitura; use
defer, módulos, dicas de recursos com moderação e dimensões nas imagens. - E-mails são outra web: tabelas de apresentação, estilos no elemento e nenhum script.
- Renderizar no servidor ou pré-renderizar entrega conteúdo pronto para pessoas e robôs.
- Escape tudo o que vem de fora e use uma CSP: o HTML é a principal porta do XSS.
- Recursos nativos recentes, como
<search>, popovers, acordeões exclusivos e comandos de botão, substituem componentes inteiros feitos com JavaScript.
Para ir além
- HTML na MDN, a referência mais completa, em português.
- HTML Living Standard, a especificação oficial do WHATWG.
- Referência de elementos HTML e formulários na web, na MDN.
- WCAG 2.2 e as práticas de autoria do ARIA (APG), do W3C.
- Tabelas acessíveis e formulários acessíveis, nos tutoriais do W3C.
- O algoritmo de leitura do HTML, na especificação, e Web Components na MDN.
- Can I email, o que cada leitor de e-mail aceita de HTML e CSS.
- NVDA, o leitor de tela gratuito para Windows.
- Como os navegadores funcionam, na MDN.
- Dados estruturados na Central da Pesquisa Google e o vocabulário do schema.org.
- Validador de HTML do W3C.
- Categorias de conteúdo, o elemento dialog e a API de comandos de botão, na MDN.
- Cálculo do nome e da descrição acessíveis (AccName) e o critério Rótulo no nome, do W3C.
- Integridade de sub-recursos e o elemento iframe, na MDN.
- HTML no web.dev, com os recursos que entraram no Baseline.
- Lei Brasileira de Inclusão, Lei 13.146/2015.
Este post foi escrito com a ajuda da Juniper, a assistente de IA da Gothor, e revisado pela nossa equipe antes de ir ao ar.


