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

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.

Rodolfo Mori15 min de leitura

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

abertura conteúdo fechamento nome do elemento atributo = "valor" <a href="/catalogo"> Dom Casmurro </a>

Um link para a ficha de um livro da Beira-Rio usa as quatro peças de uma vez:

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

js
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}`);
}
nome da tag: A conteúdo: Dom Casmurro quantos atributos: 3 class = cartao href = /catalogo/dom-casmurro title = Ficha do livro

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.

html
<p>Novidade da semana <img src="/capas/grande-sertao.jpg" alt="Capa de Grande Sertão">esgotado</img></p>
<p>Novidade da semana <img src="/capas/grande-sertao.jpg" alt="Capa de Grande Sertão">esgotado</p> filhos do parágrafo: 3 filhos da img: 0

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.

html
<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'>
sem aspas, sem espaço: src="/capas/dom-casmurro.jpg" | alt="Capa" sem aspas, com espaço: src="/capas/dom" | casmurro.jpg="" | alt="Capa" aspas simples: 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:

html
<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 atributo: "" → marcado: true #b atributo: "checked" → marcado: true #c atributo: "false" → marcado: true #d atributo: null → marcado: false

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:

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

jeito 1: <img src="/capas/dom-casmurro.jpg" alt="Capa de Dom Casmurro"> jeito 2: <img src="/capas/dom-casmurro.jpg" alt="Capa de Dom Casmurro"> jeito 3: <img src="/capas/dom-casmurro.jpg" alt="Capa de Dom Casmurro"> jeito 4: <img src="/capas/dom-casmurro.jpg" alt="Capa de Dom Casmurro"> jeito 5: <img src="/capas/dom-casmurro.jpg" alt="Capa de Dom Casmurro"> jeito 6: <img src="/capas/dom-casmurro.jpg" alt="Capa de Dom Casmurro"> todos iguais? true

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 outerHTML devolve. O A maiúsculo lá do começo não contradiz isso: quem põe em maiúsculas é a propriedade tagName, na hora de responder. No mesmo elemento, link.localName — o nome como ele está guardado — devolve a. 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:

bash
npx prettier@3.9.6 seis-jeitos.html
[error] seis-jeitos.html: SyntaxError: Opening tag "img" not terminated. (2:1) [error] 1 | <IMG SRC="/capas/dom-casmurro.jpg" ALT="Capa de Dom Casmurro"> [error] > 2 | <img src=/capas/dom-casmurro.jpg alt="Capa de Dom Casmurro"> [error] | ^^^^^^^^^^ [error] 3 | <img src='/capas/dom-casmurro.jpg' alt='Capa de Dom Casmurro'>

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

bash
grep -v "src=/capas" seis-jeitos.html > cinco-jeitos.html
npx prettier@3.9.6 cinco-jeitos.html
<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" />

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

html
<img src="/capas/dom-casmurro.jpg" src="/capas/placeholder.jpg" alt="Capa de Dom Casmurro">
atributos que sobraram: 2 src: /capas/dom-casmurro.jpg <img src="/capas/dom-casmurro.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:

html
<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>
id: ficha-101 classes: [ 'ficha', 'destaque' ] lang do artigo: pt-BR | lang do parágrafo em inglês: en title do h2: Machado de Assis, 1899 tabindex do span: 0 parágrafo oculto — hidden: true parágrafo oculto — textContent: Estoque interno: 3 exemplares na loja da Rua Sete

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:

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

js
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'));
getElementById: Dom Casmurro querySelectorAll("#destaque"): 2 querySelectorAll(".livro"): 3 className: livro esgotado classList: [ 'livro', 'esgotado' ] tem a classe esgotado? true

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, for de um label, um getElementById — use id.
  • 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:

html
<livro titulo="Vidas Secas">Graciliano Ramos</livro>
o parser criou o elemento? true classe do elemento inventado: HTMLUnknownElement classe de um elemento real: HTMLImageElement conteúdo: Graciliano Ramos atributo inventado: Vidas Secas

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:

html
<li class="livro" preco="49.90" data-preco="49.90">Vidas Secas</li>
o parser guardou? 49.90 virou propriedade? undefined e o data-preco? 49.90 achei pelo seletor de atributo: 1

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.

html
<section class="promo">
  <!-- cupom interno da equipe: BEIRARIO30 -->
  <p>Frete grátis acima de R$ 120</p>
</section>
nodeType 3 (#text): "" nodeType 8 (#comment): "cupom interno da equipe: BEIRARIO30" nodeType 3 (#text): "" nodeType 1 (P): "Frete grátis acima de R$ 120" nodeType 3 (#text): "" --- textContent da seção: "Frete grátis acima de R$ 120" innerHTML entregue ao navegador:

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

html
<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>
links encontrados: 0 parágrafos encontrados: 0 o body inteiro virou isto: <h2>Estante de clássicos</h2>

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:

  1. 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.
  2. 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.
  3. 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.

Ver todos os vídeos do canal
  • html
  • tags
  • atributos
  • id
  • class

Perguntas frequentes

HTML diferencia maiúscula de minúscula em algum lugar?
Nos nomes, não: IMG e img são a mesma tag, e HREF e href são o mesmo atributo. Nos valores, sim. A classe "Livro" e a classe "livro" são classes diferentes, e um arquivo chamado Capa.jpg não é encontrado por src="capa.jpg" em servidor Linux.
O id pode ter espaço ou acento?
Espaço não. Sem aspas, o espaço separa atributos e o id é cortado ao meio. Com aspas o valor é guardado inteiro, mas nenhum seletor alcança ele: "#ficha um" é lido pelo CSS como "um dentro de #ficha" e não acha nada. Acento a especificação aceita, mas a convenção do mercado é ASCII em minúsculas com hífen, como ficha-dom-casmurro.
Esqueci de fechar uma div. O navegador avisa?
Não avisa nada. Ele fecha a div sozinho no fim do bloco pai, e tudo que vinha depois vira filho dela. O sintoma é o layout errado sem nenhuma mensagem no console — abra o inspetor e olhe a árvore, não o arquivo.
Preciso decorar todas as tags e atributos?
Não. São mais de cem tags e centenas de atributos, e ninguém guarda isso de cabeça. Decore a anatomia e os atributos globais; o resto você consulta na hora, como todo profissional faz.

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 Prettier 3.9.6, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — Atributos globais — developer.mozilla.org
  2. HTML Standard — Attributes — html.spec.whatwg.org

Continue por aqui