Tags e atributos HTML: anatomia de um elemento
Abertura, fechamento, conteúdo e atributos: como ler qualquer tag HTML, quais são as tags vazias e quais atributos funcionam em todas elas.
Todo elemento HTML tem a mesma anatomia: uma tag de abertura que carrega o nome e os atributos, o conteúdo no meio e uma tag de fechamento. São quatro peças, e elas se repetem nas mais de cem tags que existem — por isso decorar tag é perda de tempo, e entender a anatomia é o que faz você ler qualquer HTML.
Os exemplos deste artigo montam o site da Livraria Beira-Rio: ficha de livro, capa, estante e cupom. É o mesmo site de estrutura de uma página HTML, a lição anterior desta trilha.
Cada exemplo aqui mostra não o que eu escrevi, mas o que o navegador entendeu: passei todo o HTML deste artigo pelo jsdom 30.0.1 no Node 24.16.0, que monta a árvore com o mesmo algoritmo de parsing que o HTML Standard especifica — e, no fim, pelo Prettier 3.9.6, para ver o que o formatador acha do que o navegador aceitou. Toda saída abaixo de um bloco saiu desse terminal.
Pense numa caixa com etiqueta e conteúdo
Uma encomenda tem o tipo do pacote, informações na etiqueta e algo guardado
dentro. Um elemento HTML segue a mesma leitura: o nome da tag diz que tipo
de elemento é; os atributos configuram ou identificam; o conteúdo fica
entre a abertura e o fechamento. Em <a href="/contato">Fale conosco</a>, a
é o elemento de link, href informa o destino e “Fale conosco” é o conteúdo.
A caixa é só uma imagem mental — algumas tags, como img, não carregam conteúdo
entre duas pontas e por isso são elementos vazios. Pegue qualquer linha de HTML
deste artigo e separe essas três partes antes de tentar decorar o que ela faz.
Se você consegue apontar nome, atributos e conteúdo, já consegue pesquisar uma
tag desconhecida sem tratar a sintaxe como um hieróglifo.
A anatomia: quatro peças e nada mais
Um link para a ficha de um livro da Beira-Rio usa as quatro peças de uma vez:
<a class="cartao" href="/catalogo/dom-casmurro" title="Ficha do livro">Dom Casmurro</a>A tag de abertura vai de < até >. O primeiro pedaço dentro dela é o nome
do elemento (a). Depois do nome vêm os atributos, cada um no formato
nome="valor", separados por espaço. O que está entre a abertura e o fechamento
é o conteúdo. A tag de fechamento repete o nome com uma barra na frente e
nunca leva atributo.
Não precisa acreditar em mim. Dá para perguntar ao parser:
import { JSDOM } from 'jsdom';
const html = `<a class="cartao" href="/catalogo/dom-casmurro" title="Ficha do livro">Dom Casmurro</a>`;
const { document } = new JSDOM(html).window;
const link = document.querySelector('a');
console.log('nome da tag:', link.tagName);
console.log('conteúdo:', link.textContent);
console.log('quantos atributos:', link.attributes.length);
for (const attr of link.attributes) {
console.log(` ${attr.name} = ${attr.value}`);
}Três atributos, cada um virou um par de nome e valor. E repare no A maiúsculo:
eu escrevi minúsculo. Guarde essa estranheza, ela volta daqui a pouco.
Uma regra que vale para tudo: o nome do atributo diz que informação é
aquela, e o valor diz qual. href é “para onde este link vai”;
/catalogo/dom-casmurro é o endereço. Quando você não souber para que serve um
atributo, é essa pergunta que você tem que responder.
Elemento vazio: img, br e hr não fecham
Alguns elementos não têm conteúdo nenhum. Uma imagem não envolve texto — ela
é a coisa. Esses elementos são chamados de vazios (ou void elements), e
têm só a tag de abertura. São treze no total; os que você vai usar logo no
começo são img, br, hr, input, link e meta.
Escrever um fechamento para eles não dá erro, o que é pior do que dar: o parser simplesmente joga fora.
<p>Novidade da semana <img src="/capas/grande-sertao.jpg" alt="Capa de Grande Sertão">esgotado</img></p>O </img> sumiu da árvore. E a palavra “esgotado”, que eu tinha colocado
dentro da imagem, virou irmã dela — mais um dos três filhos do parágrafo, ao
lado do texto inicial e da própria img. A imagem ficou com zero filhos, porque
um elemento vazio não aceita filho nenhum, aconteça o que acontecer.
Todo o resto — alt, src, largura, carregamento — é assunto de
imagens em HTML. Aqui interessa só a forma da tag.
Aspas: quando dá para omitir e por que não vale a pena
Você vai ver, em código antigo e em tutorial de internet, atributo sem aspas. Isso é HTML válido em alguns casos. O problema é saber quais.
<img src=/capas/dom-casmurro.jpg alt=Capa>
<img src=/capas/dom casmurro.jpg alt=Capa>
<img src='/capas/dom casmurro.jpg' alt='Capa de Dom Casmurro'>A primeira linha funcionou. A segunda é um desastre silencioso: como o espaço é
o que separa um atributo do outro, o parser cortou o caminho no meio, ficou com
src="/capas/dom" e transformou casmurro.jpg num atributo novo, sem valor.
A imagem não carrega e o console não acusa nada de anormal na tag — só um 404 do
arquivo /capas/dom.
A terceira linha mostra que aspas simples funcionam igual às duplas. Repare que o parser devolveu as três normalizadas com aspas duplas, sempre.
Atributo booleano: checked="false" continua marcado
Existe uma família de atributos em que o valor não importa: só a presença
conta. checked, disabled, required, hidden, selected e readonly são
assim. Quem vem de outra linguagem tenta desligar um deles escrevendo "false",
e é aí que a coisa quebra.
O formulário de reserva da livraria, com quatro caixas de seleção:
<input type="checkbox" id="a" checked>
<input type="checkbox" id="b" checked="checked">
<input type="checkbox" id="c" checked="false">
<input type="checkbox" id="d">A caixa #c está marcada. O "false" não desligou nada — ele é só um texto
qualquer, e o que o navegador olhou foi o fato de o atributo checked existir
na tag. A única forma de desmarcar é não escrever o atributo, como em #d.
Repare também no #a: sem valor nenhum, o parser guardou string vazia. É por
isso que getAttribute('checked') devolve "" e não true — o valor bruto
continua sendo texto, e quem traduz para verdadeiro ou falso é a propriedade
checked.
Seis jeitos de escrever a mesma imagem
Esta é a parte que costuma resolver metade das dúvidas de quem está começando. Abaixo, a mesma capa escrita de seis maneiras diferentes — tag em caixa alta, atributo sem aspas, aspas simples, espaço em volta do sinal de igual, barra final e quebra de linha no meio da tag:
<IMG SRC="/capas/dom-casmurro.jpg" ALT="Capa de Dom Casmurro">
<img src=/capas/dom-casmurro.jpg alt="Capa de Dom Casmurro">
<img src='/capas/dom-casmurro.jpg' alt='Capa de Dom Casmurro'>
<img src = "/capas/dom-casmurro.jpg" alt = "Capa de Dom Casmurro" >
<img src="/capas/dom-casmurro.jpg" alt="Capa de Dom Casmurro" />
<img
src="/capas/dom-casmurro.jpg"
alt="Capa de Dom Casmurro"
>Passando os seis pelo parser e imprimindo o outerHTML de cada elemento:
Os seis viraram exatamente o mesmo elemento. Byte por byte. Isso responde de uma vez a um monte de discussão:
- Caixa alta na tag e no atributo não muda nada. O parser guarda o nome da
tag e o nome do atributo sempre em minúsculas, e é isso que o
outerHTMLdevolve. OAmaiúsculo lá do começo não contradiz isso: quem põe em maiúsculas é a propriedadetagName, na hora de responder. No mesmo elemento,link.localName— o nome como ele está guardado — devolvea. Escreva minúsculo; é convenção, não regra. - A barra final em elemento vazio (
<img />) é decorativa. Ela é herança do XHTML. Em HTML, o parser ignora, e ela não vira nada na árvore. - Espaço, quebra de linha e indentação dentro da tag são livres. Tag com dez atributos fica muito mais legível quebrada em várias linhas.
Se os seis são idênticos para o navegador, por que escolher um? Porque o código não é lido só pela máquina — e o formatador que roda quando você salva o arquivo tem opinião formada. Joguei os seis num arquivo e chamei o Prettier:
npx prettier@3.9.6 seis-jeitos.htmlO navegador aceitou o src sem aspas sem piscar. O Prettier não: ele recusa o
arquivo inteiro por causa daquela linha, e nada é formatado. É o segundo
argumento a favor das aspas, e ele não vem do navegador — vem do ferramental que
o seu time usa todo dia.
Tirando o jeito 2 do arquivo, os outros cinco formatam, e saem assim:
grep -v "src=/capas" seis-jeitos.html > cinco-jeitos.html
npx prettier@3.9.6 cinco-jeitos.htmlCinco linhas iguais: minúsculas, aspas duplas — e a barra final colocada de volta em todo elemento vazio, mesmo onde ela não estava. Não vale discutir com o formatador: ele é a convenção do projeto virando arquivo. Ser previsível vale mais que estar certo em seis jeitos.
Atributo repetido: o parser fica com o primeiro
Copiar e colar uma tag grande e acabar com o mesmo atributo duas vezes acontece mais do que parece. O que o navegador faz é uma regra só, e é bom saber qual:
<img src="/capas/dom-casmurro.jpg" src="/capas/placeholder.jpg" alt="Capa de Dom Casmurro">Dos três atributos escritos, sobraram dois. O primeiro src venceu e o
segundo foi descartado na hora do parsing — não sobra nem vestígio dele na
árvore, e nada é dito no console.
Isso é armadilha na hora de depurar: você olha o arquivo, vê o
/capas/placeholder.jpg que acabou de colar e não entende por que a página
insiste em mostrar a outra capa. A resposta está no
inspetor de elementos, não no arquivo.
Atributos globais: os que funcionam em qualquer tag
Alguns atributos são específicos — src só faz sentido em quem carrega arquivo,
href só em quem aponta para algum lugar. Outros funcionam em qualquer
elemento, e são esses que vale conhecer de cabeça.
| atributo | serve para | na Livraria Beira-Rio |
|---|---|---|
id |
dar um nome único ao elemento na página | id="ficha-101" na ficha do livro |
class |
marcar um grupo de elementos parecidos | class="ficha destaque" |
title |
texto que aparece ao parar o mouse em cima | autor e ano do livro |
lang |
idioma daquele trecho | lang="en" numa resenha em inglês |
hidden |
esconder o elemento da página | um bloco de estoque interno |
tabindex |
colocar o elemento na ordem do Tab | um span que funciona como botão |
Uma ficha usando todos eles:
<article id="ficha-101" class="ficha destaque" lang="pt-BR">
<h2 title="Machado de Assis, 1899">Dom Casmurro</h2>
<p lang="en">A masterpiece of Brazilian literature.</p>
<p hidden>Estoque interno: 3 exemplares na loja da Rua Sete</p>
<span tabindex="0">Reservar</span>
</article>Duas coisas para levar daqui.
A primeira: class="ficha destaque" é um atributo com dois valores,
separados por espaço. É o único da tabela acima em que o espaço separa valores
em vez de separar atributos — dentro das aspas de uma class, ele é o
separador da lista.
A segunda, e mais importante: o parágrafo com hidden sumiu da tela, mas o
texto continua ali, inteiro, na última linha da saída. hidden é apresentação,
não segurança. Preço de custo, estoque, nome de cliente — nada disso pode ir
para o HTML “escondido”, porque qualquer pessoa lê com dois cliques.
lang e tabindex não são enfeite: eles mudam como o leitor de tela pronuncia
o texto e em que ordem o teclado caminha pela página. O assunto é grande e tem
lição própria em acessibilidade em HTML.
id ou class: a regra que resolve a dúvida
A regra em uma frase: id é nome próprio, class é categoria. Um id vale
para um elemento e só um na página inteira; a mesma class pode marcar cem
elementos.
O navegador, porém, não impede você de repetir um id — ele só passa a se
comportar de um jeito que confunde. Uma estante com o mesmo id em dois itens:
<ul class="estante">
<li id="destaque" class="livro esgotado">Dom Casmurro</li>
<li id="destaque" class="livro">Grande Sertão: Veredas</li>
<li class="livro">Vidas Secas</li>
</ul>Salvando esse bloco como estante.html e apontando para ele o mesmo inspetor do
começo do artigo:
import { JSDOM } from 'jsdom';
import { readFileSync } from 'node:fs';
const { document } = new JSDOM(readFileSync('estante.html', 'utf8')).window;
console.log('getElementById:', document.getElementById('destaque').textContent);
console.log('querySelectorAll("#destaque"):', document.querySelectorAll('#destaque').length);
console.log('querySelectorAll(".livro"):', document.querySelectorAll('.livro').length);
const primeiro = document.querySelector('.livro');
console.log('className:', primeiro.className);
console.log('classList:', [...primeiro.classList]);
console.log('tem a classe esgotado?', primeiro.classList.contains('esgotado'));Olhe a contradição: getElementById devolveu um elemento, o primeiro, e
querySelectorAll('#destaque') achou dois. A página tem dois elementos com
o mesmo nome próprio, e cada API responde de um jeito. É exatamente o tipo de
bug que some quando você recarrega e volta quando o backend muda a ordem da
lista.
Na prática, decida assim:
- Precisa apontar para aquele elemento específico — âncora na URL,
forde umlabel, umgetElementById— useid. - Precisa aplicar estilo ou comportamento a um conjunto — use
class. - Na dúvida, use
class. Ela nunca fica errada por repetição, e é com ela que a especificidade dos seletores CSS trabalha no dia a dia.
Do lado do JavaScript, className devolve a string crua e classList devolve a
lista já quebrada, com contains, add e remove prontos. A escolha de qual
usar aparece com detalhe em
querySelector e getElementById.
Tag e atributo que você inventa
O HTML não tem uma lista fechada que o navegador cobre na porta. Se você escrever uma tag que não existe, ele cria o elemento do mesmo jeito:
<livro titulo="Vidas Secas">Graciliano Ramos</livro>O elemento existe, tem conteúdo e guardou o atributo. Só que ele nasceu como
HTMLUnknownElement — uma caixa vazia, sem comportamento nenhum. Um img de
verdade é HTMLImageElement e sabe carregar arquivo, disparar onload,
responder a width. O livro inventado não sabe nada. Ele estiliza no CSS e
não faz mais nada.
Com atributo é parecido. Suponha que você queira guardar o preço no próprio item da estante:
<li class="livro" preco="49.90" data-preco="49.90">Vidas Secas</li>O preco foi guardado e é achável por seletor de atributo, mas não vira
propriedade do elemento no JavaScript — item.preco é undefined. O
data-preco guarda a mesma coisa e aparece no dataset. É para isso que o
prefixo data- existe: um espaço oficialmente reservado para o dado que é seu,
sem risco de colidir com um atributo que a especificação invente amanhã. A
lição de data attributes trata só disso.
Comentário: esconde do leitor, não do curioso
Comentário em HTML abre com <!-- e fecha com -->. Ele serve para deixar
recado para outra pessoa da equipe — ou para você daqui a três meses.
<section class="promo">
<!-- cupom interno da equipe: BEIRARIO30 -->
<p>Frete grátis acima de R$ 120</p>
</section><!– cupom interno da equipe: BEIRARIO30 –> <p>Frete grátis acima de R$ 120</p>
O comentário virou um nó de verdade na árvore, de nodeType 8, convivendo com o
parágrafo (nodeType 1) e com os espaços em branco (nodeType 3). Ele não
aparece na tela e não entra no textContent — mas está lá, palavra por palavra,
no HTML que sai pela rede.
O erro mais comum: a aspa que engole o resto da página
De todos os erros de sintaxe possíveis numa tag, o que mais assusta iniciante é a aspa que ficou faltando. O sintoma é desproporcional ao deslize: metade da página desaparece, e o console não diz nada.
Falta a aspa de fechamento no href:
<h2>Estante de clássicos</h2>
<a href="/catalogo/dom-casmurro>Dom Casmurro</a>
<p>Entrega em 2 dias úteis para toda a cidade.</p>
<p>Retirada na loja da Rua Sete, sem taxa.</p>Só o título sobreviveu. Nem o link nem os dois parágrafos existem na árvore.
O motivo é mecânico, e entender ele tira o susto: ao encontrar a aspa de
abertura, o parser entra no modo “estou lendo o valor de um atributo” e só sai
desse modo na próxima aspa. Como não existe outra aspa no arquivo, ele leu o
resto do documento inteiro como se fosse o valor do href — inclusive o > que
fecharia a tag. Sem esse >, a tag <a> nunca terminou, e nada depois dela
chegou a virar elemento.
Como diagnosticar em dez segundos:
- Olhe o último elemento que apareceu na página. O erro está na linha logo depois dele — não onde a página parece quebrada.
- Repare no editor: com realce de sintaxe, todo o resto do arquivo fica colorido como texto de atributo. É o sinal mais visível que existe.
- Se o problema for o contrário — a página aparece toda, mas o layout embaralha —, aí é tag aberta e não fechada. O parser fecha sozinho no fim do bloco pai, e tudo que vinha depois vira filho dela, sem nenhum aviso.
O que vem depois
Você já tem o suficiente para ler qualquer arquivo .html que aparecer na
frente: identificar o nome do elemento, separar atributo de conteúdo, saber o
que o navegador vai normalizar e o que ele vai descartar. Daqui, o caminho é
conhecer as tags de conteúdo uma a uma — título, parágrafo, lista, link, imagem,
tabela, formulário —, sempre com essa mesma anatomia por baixo. O
guia de HTML mostra a ordem em que elas entram e onde cada
assunto aparece na trilha.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
HTML diferencia maiúscula de minúscula em algum lugar?
O id pode ter espaço ou acento?
Esqueci de fechar uma div. O navegador avisa?
Preciso decorar todas as tags e atributos?
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 Node 24.16.0, jsdom 30.0.1 e Prettier 3.9.6, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — Atributos globais — developer.mozilla.org
- HTML Standard — Attributes — html.spec.whatwg.org



