HTML semântico: header, nav, main, section, article e footer
Quando trocar a div por uma tag com significado, a diferença real entre section e article e o que muda para leitor de tela e para o Google.
HTML semântico é escolher a tag pelo que o conteúdo é, e não embrulhar tudo
em div. <header>, <nav>, <main>, <section>, <article>, <aside> e
<footer> desenham na tela exatamente o mesmo que uma div — a diferença
aparece na árvore de acessibilidade, que é o que leitor de tela, modo de leitura
e robô de busca realmente leem.
Para essa lição não ficar no discurso, eu montei a mesma página — o site da Clínica Veterinária Pata Amiga — nas duas versões e li a árvore de acessibilidade do Chromium 151 direto pelo protocolo de depuração do navegador. Os números do artigo saíram desse experimento.
O nome técnico é semântica nativa: a própria tag fornece papel e estrutura ao documento. Em palavras simples, ela continua dizendo o que o bloco é mesmo quando cor, posição e tamanho deixam de ser pistas.
As placas que continuam falando sem a pintura
Num hospital, duas portas podem ter a mesma cor, mas as placas “Recepção” e
“Emergência” mudam como cada espaço é encontrado e usado. div é uma porta sem
placa; nav, main, article e aside já carregam um papel. O CSS pode pintar
todas do mesmo jeito, mas a árvore de acessibilidade preserva as diferenças.
Antes de comparar as duas versões, liste os pontos de referência que espera ouvir: navegação, conteúdo principal, artigo e conteúdo complementar. Depois abra a árvore de acessibilidade e marque quais apareceram. A conferência mede a semântica sem depender do visual.
Esta é a versão “sopa de div”. Todo mundo já escreveu uma assim, e ela não tem nada de errado visualmente: com CSS, fica idêntica à outra.
<div class="topo">
<div class="logo">Clínica Pata Amiga</div>
<div class="menu">
<div class="item"><a href="/servicos">Serviços</a></div>
<div class="item"><a href="/equipe">Equipe</a></div>
<div class="item"><a href="/dicas">Dicas</a></div>
</div>
</div>
<div class="conteudo">
<div class="titulo-pagina">Clínica Veterinária Pata Amiga</div>
<div class="bloco">
<div class="titulo-bloco">Serviços</div>
<div class="card">Consulta e vacinação</div>
<div class="card">Exames de imagem</div>
</div>
<div class="bloco">
<div class="titulo-bloco">Dicas do veterinário</div>
<div class="post">
<div class="titulo-post">Quando o gato para de comer</div>
<div class="data">12/03/2026</div>
<p>Gato que passa 24 horas sem comer já é caso de consulta.</p>
</div>
</div>
<div class="lateral">
<div class="titulo-bloco">Plantão 24h</div>
<p>Emergência: (11) 4002-8922</p>
</div>
</div>
<div class="rodape">
<div class="endereco">Rua das Acácias, 120 — São Paulo</div>
</div>E esta é a mesma página com as tags que significam alguma coisa. Repare que a quantidade de linhas é praticamente a mesma — não é mais trabalho, é outra escolha de nome:
<header>
<a class="logo" href="/">Clínica Pata Amiga</a>
<nav aria-label="Principal">
<ul>
<li><a href="/servicos">Serviços</a></li>
<li><a href="/equipe">Equipe</a></li>
<li><a href="/dicas">Dicas</a></li>
</ul>
</nav>
</header>
<main>
<h1>Clínica Veterinária Pata Amiga</h1>
<section aria-labelledby="t-servicos">
<h2 id="t-servicos">Serviços</h2>
<p>Consulta e vacinação</p>
<p>Exames de imagem</p>
</section>
<section aria-labelledby="t-dicas">
<h2 id="t-dicas">Dicas do veterinário</h2>
<article>
<h3>Quando o gato para de comer</h3>
<p><time datetime="2026-03-12">12 de março de 2026</time></p>
<p>Gato que passa 24 horas sem comer já é caso de consulta.</p>
</article>
</section>
<aside aria-label="Plantão 24h">
<h2>Plantão 24h</h2>
<p>Emergência: (11) 4002-8922</p>
</aside>
</main>
<footer>
<address>Rua das Acácias, 120 — São Paulo</address>
</footer>Os dois arquivos têm o mesmo esqueleto completo em volta — <!doctype html>,
<html lang="pt-BR"> e <head>, como na
estrutura de uma página HTML. Acima está só o miolo
do <body>.
Agora a medição. O script abaixo abre cada arquivo no Chromium, pede a árvore de acessibilidade inteira e filtra só os nós cujo papel é uma região de referência — o nome técnico é landmark:
import { chromium } from 'playwright-core';
import { pathToFileURL } from 'node:url';
import path from 'node:path';
const LANDMARKS = new Set([
'banner', 'navigation', 'main', 'region',
'complementary', 'contentinfo', 'form', 'search',
]);
const navegador = await chromium.launch();
const pagina = await navegador.newPage();
const cdp = await pagina.context().newCDPSession(pagina);
for (const arquivo of process.argv.slice(2)) {
await pagina.goto(pathToFileURL(path.resolve(arquivo)).href);
await cdp.send('Accessibility.enable');
const { nodes } = await cdp.send('Accessibility.getFullAXTree');
const regioes = nodes.filter((n) => LANDMARKS.has(n.role?.value) && !n.ignored);
console.log(`${arquivo} — ${regioes.length} regiões`);
for (const n of regioes) {
console.log(` ${n.role.value}${n.name?.value ? ` "${n.name.value}"` : ''}`);
}
}
await navegador.close();Zero contra sete. As duas páginas mostram o mesmo texto para quem enxerga; para quem navega pela estrutura, a primeira é um bloco único e indivisível.
Repare também nos nomes entre aspas. Não é o navegador inventando: navigation,
region e complementary herdaram o nome do aria-label ou do título apontado
por aria-labelledby. Já banner, main e contentinfo saem sem nome por um
motivo bem mais simples — ninguém deu nome a eles. Não é uma limitação da tag:
um <header aria-label="Topo do site"> sai da árvore como banner "Topo do site". Acontece que, existindo um de cada por página, o nome não desempata
nada, e por isso quase ninguém escreve.
Trocar div por section não muda um pixel
Antes de continuar, é bom tirar um medo do caminho: nenhuma dessas tags traz
estilo escondido. Rodei getComputedStyle em cada uma delas, recém-nascidas,
sem CSS nenhum:
const r = await pagina.evaluate(() =>
[...document.body.children].map((el) => {
const s = getComputedStyle(el);
return `${el.localName.padEnd(9)} display: ${s.display.padEnd(7)} font-size: ${s.fontSize.padEnd(6)} font-style: ${s.fontStyle}`;
}),
);
console.log(r.join('\n'));As sete tags de estrutura saem com o mesmo display: block e o mesmo
font-size da div — e, medindo junto, com margin e padding zerados também.
As exceções são honestas e fáceis de lembrar: address vem em itálico, time é
inline porque ele marca um pedaço de frase e não um bloco, e figure é a única
que chega com espaçamento embutido (margin: 16px 40px na folha de estilo padrão
do Chromium — a origem daquele recuo que ninguém pediu na legenda de imagem). Se
block e inline mudam o layout por causa do CSS; antes dessa etapa, você pode
ver como o navegador monta o DOM
a partir da estrutura.
header, nav, main e footer: os pontos de referência da página
Essas quatro são as mais fáceis, porque cada uma responde a uma pergunta óbvia.
| tag | papel exposto | o que vai dentro | quantidade por página |
|---|---|---|---|
header |
banner |
logo, nome do site, busca, menu | um, no topo do body |
nav |
navigation |
blocos de links de navegação | quantos você precisar |
main |
main |
o conteúdo único daquela página | exatamente um |
footer |
contentinfo |
endereço, direitos, links legais | um, no fim do body |
A regra de ouro do main é o que sobra depois de tirar o que se repete: se o
bloco aparece igual em todas as páginas do site, ele é header, nav ou
footer. O que muda de página para página é main.
<body>
<header>…logo e navegação, iguais em todo o site…</header>
<main>…só o que é desta página…</main>
<footer>…endereço e direitos, iguais em todo o site…</footer>
</body>Existem mais duas regiões que aparecem quase sem esforço. O <form> expõe o
papel form, e o <search> — a tag que o HTML ganhou justamente para o bloco de
busca — expõe search, o mesmo papel de um <div role="search">.
O form tem um porém que vale medir, porque é o mesmo porém da section: sem
nome acessível ele não conta como região de verdade. Dois formulários soltos no
body, um sem nome e outro com aria-label, passados pela regra region do
axe:
<form>
<label for="tutor">Nome do tutor</label>
<input id="tutor">
</form>
<form aria-label="Agendamento">
<label for="pet">Nome do pet</label>
<input id="pet">
</form>Só o primeiro aparece. O segundo, com nome, já é um destino de navegação — então o formulário de agendamento da clínica entra na conta de regiões desde que você o nomeie, e a validação de formulário sem JavaScript continua funcionando igual.
section ou article: a pergunta que resolve a dúvida
Esta é a dúvida que mais trava gente, e ela tem um teste de uma frase:
Esse pedaço faria sentido sozinho, publicado em outro lugar?
Se faz, é article. A dica “Quando o gato para de comer” faria sentido no feed
RSS da clínica, num e-mail, no card de outra página. É article.
Se não faz — se ele só existe como parte temática desta página — é section.
“Serviços” não é um conteúdo autônomo: é um capítulo da home da clínica.
| pergunta | section |
article |
|---|---|---|
| faz sentido fora da página? | não | sim |
| precisa de título próprio | sim, sempre | sim, sempre |
| exemplos na clínica | Serviços, Equipe, Onde estamos | uma dica do blog, uma avaliação de cliente |
| papel exposto | region (só com nome, ver adiante) |
article, sempre |
E os dois se aninham na direção natural: uma section agrupa vários article.
Nunca o contrário como regra geral.
<main>
<h1>Dicas do veterinário</h1>
<section aria-labelledby="t-gatos">
<h2 id="t-gatos">Gatos</h2>
<article>
<h3>Quando o gato para de comer</h3>
<p><time datetime="2026-03-12">12 de março de 2026</time></p>
</article>
<article>
<h3>Bola de pelo: o que é normal</h3>
<p><time datetime="2026-02-28">28 de fevereiro de 2026</time></p>
</article>
</section>
</main>Pedindo a árvore de acessibilidade completa desse arquivo — agora com a tag de origem ao lado do papel:
Cada article aparece como uma unidade fechada, com o próprio título dentro — e
não como dois h3 soltos embaixo do h2. É essa fronteira que faz o leitor de
tela anunciar onde uma dica começa e onde ela acaba, em vez de despejar o texto
das duas emendado. A hierarquia de h1, h2 e h3 que sustenta esse desenho
está detalhada em títulos e parágrafos em HTML.
A armadilha: section sem nome não vira região nenhuma
Aqui está o detalhe que quase nenhum tutorial conta, e que eu só acreditei
depois de medir. <article> sempre expõe o papel article. <section>, não:
ela só vira region se tiver um nome acessível. Um <h2> dentro dela não
serve como nome — é preciso aria-labelledby apontando para o título, ou
aria-label.
<main>
<section>
<h2>Vacinação</h2>
<p>Antirrábica e V10.</p>
</section>
<section aria-labelledby="banho">
<h2 id="banho">Banho e tosa</h2>
<p>Com hora marcada.</p>
</section>
</main>Duas section visualmente idênticas. Passando o mesmo script de landmarks:
A primeira section simplesmente não existe na lista de regiões. Ela não some
da página nem quebra nada — o texto continua lá, dentro de main — mas ela não
é um destino para quem navega por regiões.
aside, figure, time e address
O aside é conteúdo relacionado que não faz parte do fio principal: o box de
plantão, a caixa de “leia também”, o aviso lateral. Ele também tem um
comportamento condicional, e eu testei junto com as outras três:
<main>
<article>
<h2>Quando o gato para de comer</h2>
<p>Publicado em <time datetime="2026-03-12">12 de março de 2026</time></p>
<figure>
<img src="gato.webp" alt="Gato cinza deitado ao lado do pote de ração cheio">
<figcaption>Recusa de ração por mais de 24 horas.</figcaption>
</figure>
<aside>
<p>Filhote não espera 24 horas: leve na mesma hora.</p>
</aside>
</article>
<aside aria-label="Plantão 24h">
<h2>Plantão 24h</h2>
<p>(11) 4002-8922</p>
</aside>
</main>
<footer>
<address>
Clínica Pata Amiga — Rua das Acácias, 120, São Paulo<br>
<a href="tel:+551140028922">(11) 4002-8922</a>
</address>
</footer>Quatro leituras desse resultado:
- O
asidedentro doarticlesaiu comogeneric— papel nenhum. A regra junta lugar e nome: fora dearticle,section,nave de outroaside, oasideé semprecomplementary, com nome ou sem ele (o de cima está dentro demain, emainnão conta para essa regra). Dentro de uma dessas quatro tags, só viracomplementaryse você nomear. Sem nome, é uma caixa qualquer — que é exatamente o certo para um comentário lateral de uma dica. figureexpõefigure, e afigcaptionfica presa a ela. É por isso que legenda de imagem se escreve assim, e não com umadivembaixo doimg.timeexpõe o papeltime. O que importa nele é o atributodatetime, em formato de máquina (2026-03-12), enquanto o texto visível pode estar escrito como você quiser.addressviragroup— e cuidado com o significado dele:addressé o contato do autor daquele conteúdo ou da página, não qualquer endereço postal. O endereço de entrega de um pedido não éaddress.
header dentro de article não é o header da página
Uma pergunta que aparece cedo: posso ter mais de um header? Pode — e o
navegador sabe diferenciar. header e footer só viram banner e
contentinfo quando são de primeiro nível; dentro de article ou section,
eles ganham outro papel.
<header>
<p>Clínica Pata Amiga</p>
</header>
<main>
<article>
<header>
<h2>Quando o gato para de comer</h2>
</header>
<p>Texto da dica.</p>
<footer>
<p>Dra. Marina Toledo</p>
</footer>
</article>
</main>
<footer>
<address>Rua das Acácias, 120</address>
</footer>O de cima é banner, o de dentro é sectionheader. Mesma tag, papéis
diferentes, decididos pelo lugar. Você não precisa fazer nada para isso
acontecer — só não pode inverter e achar que dois banner vão coexistir.
main é único; nav pode ser vários
Agora o erro mais comum de quem acabou de descobrir essas tags: sair espalhando
main porque a página tem duas áreas grandes, e deixar todo nav sem nome.
<header>
<nav>
<a href="/servicos">Serviços</a>
<a href="/dicas">Dicas</a>
</nav>
</header>
<main>
<h1>Clínica Veterinária Pata Amiga</h1>
</main>
<main>
<h2>Dicas do veterinário</h2>
</main>
<footer>
<nav>
<a href="/privacidade">Privacidade</a>
</nav>
</footer>Nada disso dá erro no navegador: a página abre, o CSS aplica, ninguém reclama. A lista de regiões conta a história:
Dois main e duas navigation sem nome nenhum. Para quem navega por regiões,
isso é o mesmo que dois arquivos chamados “documento” na mesma pasta. Passando o
axe-core 4.13, um auditor automatizado de acessibilidade, no mesmo arquivo:
import { createRequire } from 'node:module';
const axeCore = createRequire(import.meta.url).resolve('axe-core');
await pagina.goto(pathToFileURL(path.resolve(arquivo)).href);
await pagina.addScriptTag({ path: axeCore });
const r = await pagina.evaluate(() =>
axe.run(document, { runOnly: ['cat.structure', 'cat.semantics', 'cat.keyboard'] }),
);
console.log(`${arquivo}: ${r.violations.length} violacoes`);
for (const v of r.violations) console.log(` [${v.impact}] ${v.id}: ${v.help}`);A correção é curta: um main só, e um aria-label em cada nav.
<header>
<nav aria-label="Principal">…</nav>
</header>
<main>
<h1>Clínica Veterinária Pata Amiga</h1>
<nav aria-label="Nesta página">…</nav>
</main>
<footer>
<nav aria-label="Institucional">…</nav>
</footer>Três navegações, três nomes distintos. E o nome não repete a palavra “navegação”: o leitor de tela já anuncia o papel, então “Navegação principal” sairia como “navegação Navegação principal”.
div não morreu: quando ela ainda é a resposta certa
Semântica não é campeonato de quem usa menos div. div é a tag correta
sempre que o agrupamento existe só por causa do layout: um container que
centraliza, um wrapper de grid, um alvo de position: relative.
<div class="pagina">
<header>
<div class="container">
<a class="logo" href="/">Clínica Pata Amiga</a>
<nav aria-label="Principal">…</nav>
</div>
</header>
<main>
<div class="container grade">
<h1>Clínica Veterinária Pata Amiga</h1>
<section aria-labelledby="t-servicos">
<h2 id="t-servicos">Serviços</h2>
<div class="cards">
<div class="card"><p>Consulta e vacinação</p></div>
<div class="card"><p>Exames de imagem</p></div>
</div>
</section>
<aside aria-label="Plantão 24h">…</aside>
</div>
</main>
<footer>
<div class="container"><address>Rua das Acácias, 120 — São Paulo</address></div>
</footer>
</div>Sete div de layout no meio do markup semântico. O que elas fazem com a lista
de regiões:
Absolutamente nada. As div são invisíveis na árvore de acessibilidade — esse é
o superpoder delas. Se você precisa de uma caixa que não significa nada, use a
tag que não significa nada. O problema nunca foi a div; foi usá-la para
esconder um main. (Se você quer entender como essa árvore nasce do seu HTML,
como o navegador monta a árvore DOM
mostra o caminho da tag até o nó.)
Landmarks: como um leitor de tela atravessa sua página em 5 segundos
Falta juntar as pontas e explicar por que tanta insistência com essas regiões.
Quem usa leitor de tela raramente ouve a página do começo ao fim. A navegação prática é por rotor de regiões: uma tecla lista as landmarks e a pessoa pula direto para a que interessa. É o equivalente a bater o olho numa página e achar o menu em meio segundo.
Na sopa de div, essa lista está vazia — e o auditor confirma. Rodando o mesmo
axe-core nas duas versões da página da clínica:
A regra region é a mais reveladora. Pedindo o detalhe dela, o axe aponta
exatamente quais blocos ficaram órfãos:
A página inteira, dividida em três blocos que não pertencem a lugar nenhum. E
repare no segundo aviso da lista anterior: page-has-heading-one. Na versão com
div, o título da página era <div class="titulo-pagina"> — bonito na tela,
inexistente para qualquer máquina.
Esse mesmo markup é o que alimenta o modo de leitura do navegador, o resumo que
o buscador mostra e as ferramentas de acessibilidade. A conta é boa: sete
regiões e zero violação custaram duas linhas a mais que a sopa de div — 33
contra 31 no par de arquivos lá do começo.
O que vem depois
Escolha uma página que você já fez e passe o script de landmarks nela. Se o
número vier baixo, a correção costuma ser meia hora: trocar o wrapper do topo
por header, o do meio por main, o de baixo por footer, e nomear cada
nav.
Depois disso, o passo natural é o resto da acessibilidade que não aparece na
lista de regiões — alt de imagem, label de campo, ordem de foco e os
atributos ARIA que valem a pena. É o assunto de
acessibilidade em HTML, a próxima lição da
trilha de HTML. Para ver onde a semântica entra no mapa
inteiro da linguagem, o guia completo de HTML mostra a ordem
de estudo.
Perguntas frequentes
Tag semântica melhora o posicionamento no Google?
Posso usar section no lugar de div para tudo?
Preciso de um h1 dentro de cada section?
E se o projeto usa Bootstrap ou Tailwind, muda alguma coisa?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Chromium 151.0.7922.34 via playwright-core, axe-core 4.13.0, Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — Referência de elementos HTML (seccionamento de conteúdo) — developer.mozilla.org
- W3C — HTML Accessibility API Mappings 1.0 — w3.org
- W3C WAI — Landmark Regions (ARIA Authoring Practices) — w3.org


