Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

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.

Rodolfo Mori13 min de leitura

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.

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

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

js
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();
divs.html — 0 regiões semantico.html — 7 regiões banner main contentinfo navigation "Principal" region "Serviços" region "Dicas do veterinário" complementary "Plantão 24h"

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.

header → banner nav → navigation "Principal" main → main section → region "Serviços" section → region "Dicas" aside → complementary "Plantão 24h" footer → contentinfo

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:

js
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'));
div display: block font-size: 16px font-style: normal header display: block font-size: 16px font-style: normal nav display: block font-size: 16px font-style: normal main display: block font-size: 16px font-style: normal section display: block font-size: 16px font-style: normal article display: block font-size: 16px font-style: normal aside display: block font-size: 16px font-style: normal footer display: block font-size: 16px font-style: normal address display: block font-size: 16px font-style: italic figure display: block font-size: 16px font-style: normal time display: inline font-size: 16px font-style: normal

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.

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.

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

html
<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>
region: All page content should be contained by landmarks form:nth-child(1) -> Some page content is not contained by landmarks

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.

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

<main> -> main <h1> -> heading "Dicas do veterinário" <section> -> region "Gatos" <h2> -> heading "Gatos" <article> -> article <h3> -> heading "Quando o gato para de comer" <p> -> paragraph <time> -> time <article> -> article <h3> -> heading "Bola de pelo: o que é normal" <p> -> paragraph <time> -> time

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.

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

secao.html — 2 regiões main region "Banho e tosa"

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:

html
<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>
<main> -> main <article> -> article <h2> -> heading "Quando o gato para de comer" <p> -> paragraph <time> -> time <figure> -> figure <img> -> image "Gato cinza deitado ao lado do pote de ração cheio" <figcaption> -> Figcaption <aside> -> generic <p> -> paragraph <aside> -> complementary "Plantão 24h" <h2> -> heading "Plantão 24h" <p> -> paragraph <footer> -> contentinfo <address> -> group <br> -> LineBreak " " <a> -> link "(11) 4002-8922"

Quatro leituras desse resultado:

  • O aside dentro do article saiu como generic — papel nenhum. A regra junta lugar e nome: fora de article, section, nav e de outro aside, o aside é sempre complementary, com nome ou sem ele (o de cima está dentro de main, e main não conta para essa regra). Dentro de uma dessas quatro tags, só vira complementary se você nomear. Sem nome, é uma caixa qualquer — que é exatamente o certo para um comentário lateral de uma dica.
  • figure expõe figure, e a figcaption fica presa a ela. É por isso que legenda de imagem se escreve assim, e não com uma div embaixo do img.
  • time expõe o papel time. O que importa nele é o atributo datetime, em formato de máquina (2026-03-12), enquanto o texto visível pode estar escrito como você quiser.
  • address vira group — 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.

html
<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>
<header> -> banner <p> -> paragraph <main> -> main <article> -> article <header> -> sectionheader <h2> -> heading "Quando o gato para de comer" <p> -> paragraph <footer> -> sectionfooter <p> -> paragraph <footer> -> contentinfo <address> -> group

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.

html
<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.html — 6 regiões banner main main contentinfo navigation navigation

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:

js
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}`);
dois-main.html: 2 violacoes [moderate] landmark-no-duplicate-main: Document should not have more than one main landmark [moderate] landmark-unique: Landmarks should have a unique role or role/label/title (i.e. accessible name) combination

A correção é curta: um main só, e um aria-label em cada nav.

html
<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>
navs.html — 6 regiões banner main contentinfo navigation "Principal" navigation "Nesta página" navigation "Institucional"

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.

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

grid.html — 6 regiões banner main contentinfo navigation "Principal" region "Serviços" complementary "Plantão 24h"

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:

divs.html: 3 violacoes [moderate] landmark-one-main: Document should have one main landmark [moderate] page-has-heading-one: Page should contain a level-one heading [moderate] region: All page content should be contained by landmarks semantico.html: 0 violacoes

A regra region é a mais reveladora. Pedindo o detalhe dela, o axe aponta exatamente quais blocos ficaram órfãos:

region: All page content should be contained by landmarks .topo -> Some page content is not contained by landmarks .conteudo -> Some page content is not contained by landmarks .rodape -> Some page content is not contained by landmarks

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.

  • html
  • semantica
  • landmarks
  • section
  • article

Perguntas frequentes

Tag semântica melhora o posicionamento no Google?
Não existe um bônus de ranking chamado "HTML semântico". O que existe é um robô que precisa entender qual parte da página é o conteúdo e qual é menu, e que usa a marcação para isso. Uma página em que o texto principal está dentro de main é mais fácil de interpretar do que uma sopa de div — e o mesmo markup alimenta o modo de leitura, o preview do link e os leitores de tela.
Posso usar section no lugar de div para tudo?
Não. section é conteúdo temático com título próprio. Se o agrupamento existe só para receber um display grid ou um padding, a tag certa é div — ela não polui a árvore de acessibilidade justamente por não significar nada.
Preciso de um h1 dentro de cada section?
Não. Use h2 e h3 seguindo a hierarquia da página. O algoritmo de outline que renumerava títulos dentro de section nunca chegou a ser implementado pelos navegadores e foi removido da especificação — quem monta a hierarquia é o número que você escreve.
E se o projeto usa Bootstrap ou Tailwind, muda alguma coisa?
Não muda nada. Classe de utilitário e tag semântica resolvem problemas diferentes: uma pinta, a outra dá significado. Você escreve "section class='grid gap-6'" e tem os dois.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. MDN — Referência de elementos HTML (seccionamento de conteúdo) — developer.mozilla.org
  2. W3C — HTML Accessibility API Mappings 1.0 — w3.org
  3. W3C WAI — Landmark Regions (ARIA Authoring Practices) — w3.org

Continue por aqui