Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

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

Títulos e parágrafos em HTML: h1 a h6, p, br e hr

Como estruturar o texto da página com h1 a h6 e a tag p, quando usar br ou hr, e por que pular do h1 para o h4 atrapalha leitor e buscador.

Rodolfo Mori10 min de leitura

Título é h1 a h6 e parágrafo é p. O número do título não escolhe o tamanho da letra: ele escolhe a posição do assunto na hierarquia da página. Quem pinta o tamanho é o CSS, e ele muda sem mexer em nada dessa estrutura.

Todos os exemplos aqui são a página de uma clínica veterinária de bairro, a Pata Firme: serviços, vacinas, endereço. É a mesma página que continua em tags e atributos HTML, onde a anatomia de um elemento foi apresentada.

Pense no sumário de um livro

O título do livro apresenta o assunto principal. Os capítulos dividem esse assunto e os subtítulos dividem cada capítulo. h1, h2 e h3 fazem essa mesma árvore na página. O número indica nível de hierarquia, não volume da voz nem tamanho da letra. O CSS pode deixar um h2 visualmente maior sem transformá-lo no assunto principal.

A comparação com o sumário tem um limite: HTML permite de h1 a h6, mas você não precisa usar todos nem criar níveis só para “ficar bonito”. Leia os títulos da página da clínica sem os parágrafos. Eles ainda contam uma história coerente — clínica, serviços, vacinação, endereço? Esse é um teste melhor que olhar o tamanho na tela. Se a sequência pula de assunto sem pai, corrija a estrutura antes de abrir o CSS.

h1 a h6 são hierarquia, não tamanho de fonte

Seis níveis, do mais geral ao mais específico. O h1 é o assunto da página inteira; cada h2 abre um bloco desse assunto; cada h3 abre um pedaço do h2 acima dele. É uma árvore, escrita numa lista plana.

html
<h1>Clínica Veterinária Pata Firme</h1>

<h2>Serviços</h2>
<h3>Consulta de rotina</h3>
<h3>Vacinação</h3>
<h4>Cães</h4>
<h4>Gatos</h4>
<h3>Castração</h3>

<h2>Horário de atendimento</h2>
<h2>Como chegar</h2>

Essa árvore não é uma metáfora: ela existe de verdade no documento, e dá para imprimir. O script abaixo lê um arquivo HTML, pega todos os títulos na ordem em que aparecem e recua cada um conforme o nível.

js
// sumario.mjs — imprime o sumário de títulos de um arquivo HTML
import { readFile } from 'node:fs/promises';
import { JSDOM } from 'jsdom';

const arquivo = process.argv[2];
const { document } = new JSDOM(await readFile(arquivo, 'utf8')).window;
const titulos = document.querySelectorAll('h1, h2, h3, h4, h5, h6');

console.log(`${arquivo} — ${titulos.length} títulos`);

let anterior = 0;
for (const titulo of titulos) {
  const nivel = Number(titulo.tagName[1]);
  const recuo = '  '.repeat(nivel - 1);
  const pulo = anterior && nivel > anterior + 1 ? `   <-- pulou h${anterior} para h${nivel}` : '';
  console.log(`${recuo}h${nivel} ${titulo.textContent.trim()}${pulo}`);
  anterior = nivel;
}

Salve a marcação acima como clinica.html e rode node sumario.mjs clinica.html:

clinica.html — 9 títulos h1 Clínica Veterinária Pata Firme h2 Serviços h3 Consulta de rotina h3 Vacinação h4 Cães h4 Gatos h3 Castração h2 Horário de atendimento h2 Como chegar

Esse desenho é o sumário da página. Ele nasce só do número da tag — nenhum CSS participou.

E o tamanho? Cada título já vem com um tamanho padrão do navegador, mas é só um padrão. O script abaixo mede a fonte de cada tag com e sem uma folha de estilo que inverte a lógica de propósito:

js
// tamanho.mjs — o tamanho padrão de cada título, e o mesmo depois do CSS
import { JSDOM } from 'jsdom';

const marcacao = '<h1>Pata Firme</h1><h2>Serviços</h2><h3>Vacinação</h3><h6>Cães</h6><p>Texto</p>';
const css = 'h1 { font-size: 20px; } h6 { font-size: 44px; }';

function medir(html) {
  const { window } = new JSDOM(html);
  return ['h1', 'h2', 'h3', 'h6', 'p'].map((tag) => {
    const el = window.document.querySelector(tag);
    return `${tag}: ${window.getComputedStyle(el).fontSize}`;
  });
}

console.log('sem CSS  ', medir(marcacao).join('  '));
console.log('com CSS  ', medir(`<style>${css}</style>${marcacao}`).join('  '));
sem CSS h1: 32px h2: 24px h3: 18.72px h6: 10.72px p: 16px com CSS h1: 20px h2: 24px h3: 18.72px h6: 44px p: 16px

Os números da primeira linha não são arbitrários. A folha de estilo padrão que todo navegador traz define os títulos em múltiplos de em: h1 2em, h2 1.5em, h3 1.17em, h4 1em, h5 0.83em e h6 0.67em. Sobre a base de 16px é exatamente o que saiu — e repare que o h4 tem o mesmo tamanho de um parágrafo comum. Ele continua sendo um título, o que já mostra que tamanho e nível são duas coisas separadas antes mesmo de você escrever uma linha de CSS.

Com três linhas de CSS, o h6 ficou mais que o dobro do h1. O sumário da página continuou idêntico. É essa independência que responde a dúvida de todo iniciante: você não escolhe o h3 porque “o h2 ficou grande demais”. Você escolhe o h3 porque aquele assunto está um degrau abaixo do anterior, e depois pede o tamanho que quiser ao CSS.

Um h1 por página — e o que acontece quando são cinco

A recomendação de usar um h1 só por página costuma ser repetida sem explicação. A explicação aparece quando você imprime o sumário. Esta é a mesma clínica com um h1 em cada bloco, salva como clinica-cinco-h1.html e passada pelo mesmo sumario.mjs:

html
<h1>Clínica Veterinária Pata Firme</h1>
<h1>Serviços</h1>
<h1>Vacinação</h1>
<h1>Horário de atendimento</h1>
<h1>Como chegar</h1>
clinica-cinco-h1.html — 5 títulos h1 Clínica Veterinária Pata Firme h1 Serviços h1 Vacinação h1 Horário de atendimento h1 Como chegar

Sem recuo nenhum. A página tem cinco assuntos de mesma importância e nenhum deles é o assunto principal. “Vacinação” deixou de ser um pedaço de “Serviços” e virou um tópico solto, no mesmo patamar do nome da clínica.

O problema não é o buscador dar um puxão de orelha — é que a informação de que vacinação faz parte dos serviços simplesmente não está mais escrita em lugar nenhum. Quem usa a página com os olhos deduz pela posição na tela. Quem usa por áudio, não.

O parágrafo e os espaços em branco que somem só na tela

Bloco de texto é p. Cada parágrafo é um p, e o navegador já coloca o espaçamento entre eles. O que confunde no começo é a quebra de linha que você digita no editor e não aparece na tela:

html
<p>Atendemos    de segunda a sábado.
       Emergência
       24 horas.</p>

Quatro espaços seguidos, duas quebras de linha e vários recuos. Na tela, sai tudo numa frase só. Mas cuidado com a conclusão fácil: o navegador não apaga esses espaços do documento. Ele guarda tudo e apenas renderiza de forma colapsada.

js
// espacos.mjs — o que o DOM guarda e o que a tela mostra
import { JSDOM } from 'jsdom';

const html = `<p>Atendemos    de segunda a sábado.
       Emergência
       24 horas.</p>`;

const { document } = new JSDOM(html).window;
const paragrafo = document.querySelector('p');
const bruto = paragrafo.textContent;

console.log('no DOM     :', JSON.stringify(bruto));
console.log('caracteres :', bruto.length);
console.log('na tela    :', JSON.stringify(bruto.replace(/\s+/g, ' ').trim()));
console.log('caracteres :', bruto.replace(/\s+/g, ' ').trim().length);
no DOM : "Atendemos de segunda a sábado.\n Emergência\n 24 horas." caracteres : 68 na tela : "Atendemos de segunda a sábado. Emergência 24 horas." caracteres : 51

Dezessete caracteres de diferença. Eles continuam lá, no texto do elemento — quem colapsa sequências de espaço em um espaço só é a renderização, e isso é uma regra de CSS (white-space: normal), não de HTML. Por isso <pre> mostra o texto exatamente como está no arquivo: ele muda essa regra.

Isso importa no dia em que você for comparar o conteúdo de um parágrafo com JavaScript. O texto que você vê na tela e o texto que o textContent devolve não são a mesma string.

br quebra linha, e só isso

A tag br força uma quebra de linha dentro de um mesmo bloco de texto. Ela tem um uso legítimo e estreito: quando a quebra faz parte do conteúdo. Endereço, verso de poema, linha de um formulário impresso.

html
<p>Rua das Acácias, 120<br>Vila Mariana<br>São Paulo — SP</p>

<p>Levamos o pet para a sala de espera.<br><br>A consulta dura 30 minutos.<br><br>A vacina fica para o fim.</p>

O primeiro p está certo: o endereço é uma coisa só, escrita em três linhas. O segundo é a gambiarra clássica — três parágrafos separados por <br><br> para “dar um espacinho”. Os dois parecem iguais na tela. No documento, não:

js
// br.mjs — br de verdade x br usado como parágrafo
import { JSDOM } from 'jsdom';

const comBr = `<p>Rua das Acácias, 120<br>Vila Mariana<br>São Paulo — SP</p>
<p>Levamos o pet para a sala de espera.<br><br>A consulta dura 30 minutos.<br><br>A vacina fica para o fim.</p>`;

const semBr = `<p>Rua das Acácias, 120<br>Vila Mariana<br>São Paulo — SP</p>
<p>Levamos o pet para a sala de espera.</p>
<p>A consulta dura 30 minutos.</p>
<p>A vacina fica para o fim.</p>`;

for (const [nome, html] of [['com br', comBr], ['com p', semBr]]) {
  const { document } = new JSDOM(html).window;
  const paragrafos = [...document.querySelectorAll('p')];
  console.log(`${nome}: ${paragrafos.length} elementos <p>, ${document.querySelectorAll('br').length} <br>`);
  paragrafos.forEach((p, i) => console.log(`   p[${i}] ${p.textContent.length} caracteres`));
}
com br: 2 elementos <p>, 6 <br> p[0] 46 caracteres p[1] 88 caracteres com p: 4 elementos <p>, 2 <br> p[0] 46 caracteres p[1] 36 caracteres p[2] 27 caracteres p[3] 25 caracteres

Na versão com <br><br>, os três parágrafos viraram um bloco de 88 caracteres. Consequências práticas: para o CSS existe um bloco só, então não há como dar uma margin diferente ao primeiro nem alcançar os três com p:nth-of-type; o leitor de tela lê tudo como um parágrafo só; e o espaçamento entre eles vira refém da altura da linha em vez de vir de uma margin que você controla.

A regra é curta: se o espaço em branco é conteúdo, br; se é layout, p mais CSS.

hr é separação de assunto, não risquinho

A tag hr marca uma mudança de assunto dentro da mesma seção — a virada entre duas partes de um texto. O navegador desenha uma linha porque é a representação padrão dela, e não porque ela seja um elemento de desenho. Precisa de uma linha decorativa? Isso é border no CSS.

hr também é um elemento vazio: não tem conteúdo e não tem fechamento. Se você tentar colocar texto dentro dele, o parser não reclama — ele reorganiza. E, no caminho, faz uma coisa que pega muita gente: um hr encerra o parágrafo que estiver aberto.

js
// hr.mjs — hr é vazio, e ele fecha o parágrafo aberto
import { JSDOM } from 'jsdom';

const html = `<p>Consulta de rotina: R$ 180<hr>Vacina V10: R$ 95</p>
<hr>Este texto tentou morar dentro do hr</hr>`;

const { document } = new JSDOM(html).window;
console.log(document.body.innerHTML);
console.log('---');
console.log('filhos do primeiro hr:', document.querySelector('hr').childNodes.length);
<p>Consulta de rotina: R$ 180</p><hr>Vacina V10: R$ 95<p></p> <hr>Este texto tentou morar dentro do hr --- filhos do primeiro hr: 0

Leia o que saiu com calma, porque nada disso foi escrito por você. O p foi fechado antes do hr. O preço da vacina ficou solto no body, fora de qualquer parágrafo. O </p> que sobrou virou um <p></p> vazio. E o </hr> da última linha simplesmente desapareceu, porque tag de fechamento de elemento vazio não existe.

Esse conserto silencioso do parser é o mesmo mecanismo descrito em como o navegador monta a árvore DOM: o HTML nunca “dá erro de sintaxe”, ele te entrega um documento diferente do que você escreveu.

O sumário que o leitor de tela monta com os seus títulos

Quem navega por leitor de tela raramente lê a página de cima a baixo. O atalho mais usado é pedir a lista de títulos e pular direto para o trecho que interessa — é a mesma lista que o sumario.mjs imprimiu lá em cima.

Duas coisas seguem daí. A primeira: título é a tag, não a aparência. Este pedaço da clínica, salvo como pagina.html, tem um “Consulta de rotina” em negrito, dentro de um parágrafo, com toda a cara de título:

html
<h1>Clínica Veterinária Pata Firme</h1>
<p>Atendimento clínico e cirúrgico para cães e gatos na Vila Mariana.</p>

<h2>Serviços</h2>
<p class="titulo-falso"><strong>Consulta de rotina</strong></p>
<p>Avaliação completa em 30 minutos, com peso, temperatura e exame físico.</p>

<h3>Vacinação</h3>
<p>Aplicamos V10, antirrábica e giárdia, com carteirinha atualizada na hora.</p>

<h2>Como chegar</h2>
<p>Rua das Acácias, 120<br>Vila Mariana<br>São Paulo — SP</p>
pagina.html — 4 títulos h1 Clínica Veterinária Pata Firme h2 Serviços h3 Vacinação h2 Como chegar

“Consulta de rotina” não está no sumário. Para o olho, é um título; para a navegação, não existe. A tag strong marca importância dentro da frase, e isso é assunto de formatação de texto em HTML — não é hierarquia, e não ocupa o lugar de um título.

A segunda: pular nível quebra o recuo. Este é o conteúdo do primeiro exemplo outra vez, com as tags escolhidas pelo tamanho da letra em vez do nível — a clínica sai do h1 direto para h4, e h6 e h3 se misturam no meio:

html
<h1>Clínica Veterinária Pata Firme</h1>

<h4>Serviços</h4>
<h6>Consulta de rotina</h6>
<h6>Vacinação</h6>
<h3>Cães</h3>
<h3>Gatos</h3>
<h6>Castração</h6>

<h4>Horário de atendimento</h4>
<h4>Como chegar</h4>
clinica-pulada.html — 9 títulos h1 Clínica Veterinária Pata Firme h4 Serviços <-- pulou h1 para h4 h6 Consulta de rotina <-- pulou h4 para h6 h6 Vacinação h3 Cães h3 Gatos h6 Castração <-- pulou h3 para h6 h4 Horário de atendimento h4 Como chegar

Repare em “Cães” e “Gatos”. Eles deviam estar abaixo de “Vacinação”, e apareceram acima, mais próximos da margem. Como estão em h3 e a vacinação está em h6, o documento afirma que os tipos de vacina são um assunto mais amplo do que a vacinação em si. Nenhum ser humano escreveria isso de propósito — mas é exatamente o que sai quando a tag é escolhida pelo tamanho da letra.

Mais sobre esse tipo de decisão em acessibilidade em HTML, e sobre quais tags agrupam essas seções em HTML semântico.

O erro clássico: uma div dentro de um p

Este aqui aparece no primeiro projeto de quase todo mundo. O parágrafo tem um preço no meio, e você quer estilizar esse preço:

js
// div-no-p.mjs — o que o parser faz com uma div dentro de um p
import { JSDOM } from 'jsdom';

const escrito = `<p>A vacina V10 protege contra sete doenças.
  <div class="preco">R$ 95</div>
  Aplicação em 10 minutos.</p>`;

const { document } = new JSDOM(escrito).window;
console.log(document.body.innerHTML);
console.log('---');
console.log('elementos <p>:', document.querySelectorAll('p').length);
console.log('a div é filha do p?', document.querySelector('.preco').parentElement.tagName);
<p>A vacina V10 protege contra sete doenças. </p><div class="preco">R$ 95</div> Aplicação em 10 minutos.<p></p> --- elementos <p>: 2 a div é filha do p? BODY

Você escreveu um parágrafo. O documento ficou com dois — e a div não é filha de nenhum dos dois: o pai dela é o body. A frase “Aplicação em 10 minutos” ficou solta, sem parágrafo. Nenhuma mensagem de erro apareceu.

O motivo: um p só aceita conteúdo inline dentro dele. Quando o parser encontra a tag de abertura de um elemento de bloco, ele fecha o parágrafo naquele ponto exato — repare no </p> que saiu colado no <div>, com a quebra de linha e os dois espaços do recuo ainda guardados dentro do parágrafo. A mesma coisa acontece com um p dentro de outro p:

js
import { JSDOM } from 'jsdom';
const { document } = new JSDOM('<p>Horário de segunda a sexta: <p>8h às 19h</p></p>').window;
console.log(document.body.innerHTML);
<p>Horário de segunda a sexta: </p><p>8h às 19h</p><p></p>

A correção é trocar a div por um elemento inline. span é o genérico, e existe justamente para isso:

js
// div-no-p-corrigido.mjs — span é inline e continua dentro do parágrafo
import { JSDOM } from 'jsdom';

const escrito = `<p>A vacina V10 protege contra sete doenças.
  <span class="preco">R$ 95</span>
  Aplicação em 10 minutos.</p>`;

const { document } = new JSDOM(escrito).window;
console.log(document.body.innerHTML);
console.log('---');
console.log('elementos <p>:', document.querySelectorAll('p').length);
console.log('o span é filho do p?', document.querySelector('.preco').parentElement.tagName);
<p>A vacina V10 protege contra sete doenças. <span class="preco">R$ 95</span> Aplicação em 10 minutos.</p> --- elementos <p>: 1 o span é filho do p? P

Um parágrafo, o span dentro dele, nada solto no body. Se o preço realmente precisa ocupar uma linha inteira, o caminho não é a div: é o span com display: block no CSS, ou tirar o preço do parágrafo de vez.

O que vem depois

Você já monta a espinha de texto de uma página: hierarquia com h1 a h6, blocos com p, quebra de conteúdo com br e virada de assunto com hr. Isso se encaixa dentro do body que aparece na estrutura de uma página HTML.

O próximo passo da trilha de HTML é dar ênfase e semântica ao texto dentro do parágrafo — negrito, itálico, código e citação. Para ver onde essa lição entra no percurso todo, o guia completo de HTML mostra a ordem de estudo.

Prefere aprender em vídeo?

Tem uma aula sobre este assunto no nosso canal.

Ver todos os vídeos do canal
  • html
  • titulos
  • paragrafo
  • hierarquia
  • texto

Perguntas frequentes

O h1 precisa ter o mesmo texto da tag title?
Não precisa, e muitas vezes não deve. A title é o texto da aba e do resultado de busca, então costuma levar o nome do site junto. O h1 é o título do conteúdo dentro da página, e pode ser mais curto e direto.
Colocar o título dentro de section ou article reinicia a contagem de níveis?
Não. Existiu uma proposta de sumário automático em que cada section rebaixava os títulos de dentro, mas nenhum navegador chegou a implementar, e ela saiu da especificação. O nível é o número que você escreve na tag, e nada mais.
Preciso mesmo fechar a tag p?
A especificação permite omitir o fechamento quando vem outro elemento de bloco logo depois, e por isso muito HTML antigo aparece sem ele. Feche sempre: o fechamento explícito é o que deixa claro, para quem lê o arquivo, onde o parágrafo termina.
Dá para usar dois h2 seguidos, sem nenhum h3 entre eles?
Dá, e é o normal. A regra é não pular níveis para baixo. Voltar para um nível já usado, ou repetir o mesmo, é exatamente o que acontece quando uma seção termina e outra começa.

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 Node 24.16.0 + jsdom 30.0.1, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — Elementos de título (h1–h6) — developer.mozilla.org
  2. MDN — O elemento p — developer.mozilla.org
  3. HTML Standard — Optional tags — html.spec.whatwg.org
  4. W3C WAI — Headings — w3.org

Continue por aqui