Pular para o conteúdo

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.

Equipe Gothor· Publicado em · Atualizado em · 51 min de leitura
Capa do artigo com o título HTML por dentro e um editor com a estrutura de uma página, com header, main, article e footer

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 href de um link ou o alt de 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.

Linha do tempo do HTML de 1991 a 2019
De 18 marcações ao Living Standard: três décadas de HTML

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.

Diagrama de um arquivo HTML ao lado da árvore DOM correspondente, com elementos, nós de texto e atributos
O texto vira uma árvore de objetos, e o JavaScript lê e altera essa árvore

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.

Um trecho de HTML malformado, os tokens que o tokenizador emite e a árvore DOM resultante, com os elementos html, head, body e tbody criados pelo parser
O parser completa o que falta: elementos implícitos, tags fechadas e conteúdo realocado

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 como table > tr nã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: &lt; vira <, &amp; vira &, &nbsp; vira um espaço que não quebra linha e &#8212; 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.

Estrutura de uma página com header, nav, main, article, section, figure, aside e footer, ao lado dos papéis de cada região e do sumário de títulos
Cada região da página diz o que é, e o leitor de tela transforma isso em atalhos

Os elementos de estrutura mais usados:

ElementoSignificado
headero cabeçalho da página ou de uma seção
navum bloco de links de navegação
maino conteúdo principal; um por página
articleum conteúdo que faz sentido sozinho, como um post ou um produto
sectionuma seção temática, normalmente com um título
asideum conteúdo complementar, como um sumário ou links relacionados
footero rodapé da página ou de uma seção
figure e figcaptionuma imagem, diagrama ou trecho de código com a sua legenda
timeuma 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.

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.

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.br abre o programa de e-mail, e tel:+556699999999 faz 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. noopener e noreferrer protegem a página e a privacidade ao abrir outro site; nofollow, sponsored e ugc dizem 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:

ElementoPara 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çãouma ficha de produto: peso, dimensões, garantia
<blockquote> e <q>citação em bloco ou dentro da fraseo 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ânciao termo buscado, realçado nos resultados
<address>o contato de quem publica a página ou o artigoendereç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 inseridoo 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 alt descreve o que a imagem comunica. Imagem decorativa recebe alt="", vazio, para ser ignorada. Imagem sem alt faz o leitor de tela ler o nome do arquivo.
  • Rótulo em todo campo. Um <label> ligado ao campo por for e id dá 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:

  1. **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.
  2. **aria-label**, um texto escrito direto no atributo.
  3. O rótulo nativo da linguagem, como o <label> de um campo ou o alt de uma imagem.
  4. O conteúdo, para os elementos que recebem o nome do que têm dentro, como botões, links e títulos.
  5. **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 com focus(). 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ó 0 e -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:

  1. 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.
  2. 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.
  3. 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?
  4. 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.
  5. Rode um verificador automático, como o Lighthouse, que vem no Chrome, ou o axe. Eles acham contraste insuficiente, imagens sem alt e 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.

Uma tabela de vendas por loja com caption, thead, th com scope de coluna e de linha, tbody e tfoot, e o que o leitor de tela anuncia ao chegar numa célula
Cabeçalhos ligados às células: o leitor de tela anuncia a linha e a coluna de cada valor
<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.

Um formulário de newsletter renderizado, o HTML por trás, o que o leitor de tela recebe e o que vai pela rede no envio
Um formulário bem marcado ganha rótulos, teclado certo, preenchimento automático e validação sem uma linha de 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:

  1. 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.
  2. Cada <label> com for dá nome ao campo e torna o rótulo clicável.
  3. O CNPJ e o CEP usam inputmode="numeric", e não type="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.
  4. O pattern confere o formato antes do envio, e o aria-describedby faz o leitor de tela ler a dica junto com o rótulo.
  5. Os valores de autocomplete permitem ao navegador preencher razão social, e-mail, telefone e CEP com um toque.
  6. 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 novalidate no <form> desliga os balões nativos, mas mantém a validação disponível para o JavaScript pela Constraint Validation API: campo.validity.valueMissing diz se o campo obrigatório está vazio, campo.validity.patternMismatch diz se o formato não bate, form.checkValidity() confere tudo e campo.setCustomValidity('mensagem') define um erro próprio.
  • Cada mensagem fica num elemento ligado ao campo por aria-describedby, e o campo com erro recebe aria-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-invalid só 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.

Pipeline de renderização: HTML vira DOM, CSS vira CSSOM, os dois formam a render tree, seguida de layout, pintura e composição, com o efeito de async, defer e módulos no carregamento de scripts
Dos bytes aos pixels, e onde cada forma de carregar um script interrompe ou não a leitura do HTML
  1. 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.
  2. Leitura do CSS. As folhas de estilo viram o CSSOM, com a cascata resolvida: para cada elemento, qual regra vence.
  3. Render tree. DOM e CSSOM se combinam numa árvore só com o que aparece, já com os estilos calculados. Um elemento com display: none nem entra.
  4. Layout. O navegador calcula a posição e o tamanho de cada caixa na tela.
  5. Pintura. Cada caixa vira pixels: cores, textos, bordas, sombras e imagens.
  6. 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:

FormaBaixaExecutaOrdem garantida
<script>parando a leituralogo após baixarsim
<script async>em paraleloassim que baixa, interrompendo a leituranão
<script defer>em paralelodepois de ler todo o HTMLsim
<script type="module">em paralelodepois de ler todo o HTML, como o defersim

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>:

DicaO que fazQuando usar
rel="preconnect"abre cedo a conexão com outro domínio, com DNS, TCP e TLSfontes ou API em outro endereço, usados logo no início
rel="dns-prefetch"só resolve o nome do domíniodomínios usados mais tarde na página
rel="preload"baixa já, com prioridade, um recurso desta páginaa fonte do título ou a imagem principal, descobertas tarde pelo CSS
rel="modulepreload"baixa e prepara um módulo JavaScriptmódulos importados por outros módulos
rel="prefetch"baixa com baixa prioridade algo da próxima páginao 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 a description aparecem 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 canonical evita conteúdo duplicado, e a meta robots com noindex tira 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 &lt;, & vira &amp;, aspas viram entidades. Frameworks como o React fazem isso por padrão; o perigo está nas saídas de emergência, como innerHTML e dangerouslySetInnerHTML.
  • **Prefira textContent** a innerHTML ao 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 sandbox em <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 ao target="_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 popover transforma 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 inert desativa 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:

  1. O formulário com method="dialog" fecha o diálogo ao ser enviado, sem mandar nada ao servidor, e o returnValue do diálogo recebe o value do botão usado. O script lê esse valor no evento close para saber a resposta.
  2. O autofocus no "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.
  3. O aria-labelledby dá ao diálogo o nome do título, que o leitor de tela anuncia na abertura.
  4. 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.
  5. O botão que abre o diálogo usa os atributos commandfor e command, 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 antigo role="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 popovers auto abertos; manual, que só fecha por um botão ou por script; e hint, para dicas que aparecem ao passar o mouse ou ao receber foco, sem fechar os popovers auto. O popover aberto também vai para a camada superior, livre do overflow e do posicionamento dos ancestrais.
  • Acordeões exclusivos, Baseline desde setembro de 2024: vários <details> com o mesmo atributo name formam 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 commandfor e command deixam 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 evento command no 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 evento beforematch, tira o hidden e 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 @media que 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 alt e 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.
  • **lang e <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 o en de um modelo pronto numa página em português.
  • **<div> e <span> para tudo**, inclusive para botões e links.
  • **Imagens sem alt**, ou com alt="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 width e height**, 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.
  • **tabindex positivo** 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-label diferente do texto visível**, que quebra o controle por voz.
  • **<iframe> sem title** e sem sandbox quando 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> e scope, 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

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.

Comentários

Ainda não há comentários. Que tal ser o primeiro?

Entre na sua conta para comentar. É grátis.

Receba os próximos posts por e-mail

Um e-mail a cada post novo, sem spam. Você cancela quando quiser.

Para ampliar, use a roda do mouse, os botões, as teclas + e − ou o gesto de pinça; toque duas vezes para ampliar ou voltar. Com a imagem ampliada, arraste ou use as setas para mover. Esc fecha.