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

O que é HTML e como o navegador transforma tag em página

HTML é marcação, não programação: o que cada tag faz, o que o navegador faz com ela e por que um arquivo .html abre sem instalar nada.

Rodolfo Mori9 min de leitura

HTML é uma linguagem de marcação: ela não manda o computador fazer nada, ela diz o que cada pedaço de texto é — isto é um título, isto é um parágrafo, isto é um link. O navegador lê essa descrição, monta uma árvore de elementos na memória e só então pinta a tela.

Os exemplos deste artigo montam a página da Livraria Página Sete, uma livraria de bairro que ainda não tem site. O ponto de partida é o aviso que hoje está impresso e colado na porta dela.

Etiquetas numa mudança: por que marcação não é programação

Pense nas caixas de uma mudança. Escrever “cozinha”, “livros” e “frágil” não move nenhuma caixa, mas dá significado ao conteúdo e permite que cada pessoa saiba como tratar aquilo. O HTML faz esse trabalho na página: suas tags dizem “título”, “parágrafo”, “link” e “formulário”.

Voltando ao termo correto, HTML é uma linguagem de marcação. As tags descrevem estrutura e semântica; elas não criam condições nem executam laços. Na comparação entre o aviso .txt e o .html, conte quantos elementos o parser encontra em cada um. Essa saída é a prova de que marcar não é apenas mudar a aparência: é produzir uma estrutura que o navegador entende.

A sigla diz exatamente isso: HTML é HyperText Markup Language, linguagem de marcação de hipertexto. “Hipertexto” é o texto que aponta para outro texto — o link, a invenção que fez a web existir. “Marcação” é todo o resto do trabalho: dizer o que cada trecho de texto é antes de ele aparecer na tela.

Repare no que o HTML não tem: não tem if, não tem repetição, não tem variável, não tem conta. Nada dentro de um arquivo .html decide alguma coisa. Por isso “linguagem de marcação” não é um jeito educado de dizer “linguagem de programação fácil” — são categorias diferentes de ferramenta.

Uma comparação que ajuda: programar é escrever a receita; marcar é passar marca-texto amarelo no ingrediente e verde no modo de preparo. O texto continua o mesmo. O que muda é que agora alguém consegue separar as partes.

Este é o aviso da livraria, salvo como texto puro:

text
Livraria Página Sete
Rua das Acácias, 120 — aberto de terça a domingo
Clube de leitura
Toda quinta, às 19h, com café por conta da casa.

E este é o mesmo aviso, marcado:

html
<h1>Livraria Página Sete</h1>
<p>Rua das Acácias, 120 — aberto de terça a domingo</p>
<h2>Clube de leitura</h2>
<p>Toda quinta, às 19h, com café por conta da casa.</p>

Para uma pessoa lendo, os dois dizem exatamente a mesma coisa. Para o navegador, não. Vamos medir isso.

A prova: o mesmo aviso em .txt e em .html

O jsdom é uma implementação do parser de HTML e do DOM que roda dentro do Node. Ele segue a mesma especificação que o Chrome e o Firefox seguem, então a árvore que ele monta a partir de um arquivo é a árvore que o navegador montaria. Dá para contar o que sobrou de cada arquivo depois da leitura:

js
import { readFileSync } from 'node:fs';
import { JSDOM } from 'jsdom';

const contar = (conteudo) => {
  const { document } = new JSDOM(conteudo).window;
  const elementos = document.body.querySelectorAll('*').length;
  const textos = [...document.body.childNodes].filter((n) => n.nodeType === 3).length;
  return { elementos, textos };
};

console.log('aviso.txt  ->', contar(readFileSync('aviso.txt', 'utf8')));
console.log('aviso.html ->', contar(readFileSync('aviso.html', 'utf8')));
aviso.txt -> { elementos: 0, textos: 1 } aviso.html -> { elementos: 4, textos: 4 }

Quatro linhas de texto viraram um nó no arquivo .txt: um bloco só, sem divisão nenhuma. As mesmas quatro linhas marcadas viraram quatro elementos, cada um com o próprio texto dentro. Marcação não é enfeite — é literalmente estrutura de dados. Os quatro nós de texto que ainda aparecem no .html são só as quebras de linha entre as tags.

A árvore fica visível quando você percorre os nós:

js
const { document } = new JSDOM(readFileSync('aviso.html', 'utf8')).window;

const desenhar = (no, nivel = 0) => {
  for (const filho of no.childNodes) {
    if (filho.nodeType === 1) {
      console.log(`${'  '.repeat(nivel)}<${filho.tagName.toLowerCase()}>`);
      desenhar(filho, nivel + 1);
    } else if (filho.nodeType === 3 && filho.textContent.trim()) {
      console.log(`${'  '.repeat(nivel)}"${filho.textContent.trim()}"`);
    }
  }
};

desenhar(document.body);
<h1> "Livraria Página Sete" <p> "Rua das Acácias, 120 — aberto de terça a domingo" <h2> "Clube de leitura" <p> "Toda quinta, às 19h, com café por conta da casa."

Cada tag virou um galho; cada trecho de texto virou uma folha pendurada nele. Essa árvore tem nome: é o DOM, e é sobre ela que todo o resto do front-end trabalha. O caminho completo do texto até a tela é este:

aviso.html texto puro parser do navegador árvore de nós o DOM tela pintada

Se quiser ver esse processo em detalhe, ele está destrinchado em como o navegador monta a árvore DOM.

Uma tag, um significado

Cada tag carrega um significado que o navegador conhece de antemão. h1 é o título principal; h2 é um subtítulo dependente dele; p é parágrafo. Por isso dá para perguntar à página coisas que o arquivo .txt não saberia responder:

js
const { document } = new JSDOM(readFileSync('aviso.html', 'utf8')).window;

const titulos = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
console.log('títulos encontrados:', titulos.length);
for (const t of titulos) console.log(`  nível ${t.tagName[1]}: ${t.textContent}`);
console.log('parágrafos encontrados:', document.querySelectorAll('p').length);
títulos encontrados: 2 nível 1: Livraria Página Sete nível 2: Clube de leitura parágrafos encontrados: 2

O navegador não leu o sentido das palavras. Ele leu as tags, que são o rótulo que você colou em cada trecho. O nome de cada tag e as informações extras que ela aceita estão detalhados em tags e atributos HTML.

A div é a tag que não quer dizer nada

Existe uma tag genérica, a div, que serve só para agrupar. Com CSS dá para deixar uma página de div visualmente idêntica à outra. Só que idêntica na tela não é idêntica na árvore:

js
const comDiv = `
<div>Livraria Página Sete</div>
<div>Rua das Acácias, 120 — aberto de terça a domingo</div>
<div>Clube de leitura</div>
<div>Toda quinta, às 19h, com café por conta da casa.</div>`;

const comSignificado = `
<h1>Livraria Página Sete</h1>
<p>Rua das Acácias, 120 — aberto de terça a domingo</p>
<h2>Clube de leitura</h2>
<p>Toda quinta, às 19h, com café por conta da casa.</p>`;

const analisar = (rotulo, html) => {
  const { document } = new JSDOM(html).window;
  const visivel = document.body.textContent.replace(/\s+/g, ' ').trim();
  console.log(rotulo);
  console.log('  caracteres de texto na tela:', visivel.length);
  console.log('  títulos que o navegador reconhece:', document.querySelectorAll('h1,h2,h3,h4,h5,h6').length);
};

analisar('versão feita só de div:', comDiv);
analisar('versão com marcação:', comSignificado);
versão feita só de div: caracteres de texto na tela: 135 títulos que o navegador reconhece: 0 versão com marcação: caracteres de texto na tela: 135 títulos que o navegador reconhece: 2

Mesmos 135 caracteres na tela, zero títulos contra dois. Escolher a tag certa é a parte do HTML que dá trabalho e é a que separa página amadora de página profissional — assunto de HTML semântico.

Seu primeiro arquivo .html abre sem servidor nenhum

Um arquivo HTML é texto puro com outra extensão. Não tem compilação, não tem instalação, não tem terminal. Você salva e dá dois cliques.

html
<!DOCTYPE html>
<html lang="pt-BR">
  <head>
    <meta charset="UTF-8" />
    <title>Livraria Página Sete</title>
  </head>
  <body>
    <h1>Livraria Página Sete</h1>
    <p>Rua das Acácias, 120 — de terça a domingo.</p>
    <ul>
      <li>Clube de leitura toda quinta</li>
      <li>Sebo com troca de livros</li>
    </ul>
    <p><a href="/clube">Entrar no clube de leitura</a></p>
  </body>
</html>

O sistema operacional confirma que não há nada de especial nesse arquivo:

bash
file pagina.html
wc -c pagina.html
pagina.html: HTML document text, Unicode text, UTF-8 text 407 pagina.html

407 bytes de texto. E é disso que o navegador extrai o idioma da página, o nome que vai na aba e a lista de itens:

js
const dom = await JSDOM.fromFile('pagina.html');
const { document } = dom.window;

console.log('idioma declarado:', document.documentElement.lang);
console.log('codificação:', document.characterSet);
console.log('título da aba:', document.title);
console.log('filhos do <html>:', [...document.documentElement.children].map((e) => e.tagName).join(', '));
console.log('itens da lista:', document.querySelectorAll('li').length);
idioma declarado: pt-BR codificação: UTF-8 título da aba: Livraria Página Sete filhos do <html>: HEAD, BODY itens da lista: 2

O head guarda informação sobre a página e não aparece na tela; o body guarda o que aparece. Cada peça dessa estrutura mínima está explicada em estrutura de uma página HTML.

O que o navegador escreve por você

Aqui vem a parte que confunde quem está começando. Aquele esqueleto todo é obrigatório — mas se você não escrever, o navegador escreve sozinho:

js
// nenhuma dessas tags foi escrita: html, head, body
const { document } = new JSDOM('<h1>Livraria Página Sete</h1>').window;

console.log('estrutura montada:', document.documentElement.outerHTML);
console.log('o h1 caiu dentro de:', document.querySelector('h1').parentElement.tagName);
estrutura montada: <html><head></head><body><h1>Livraria Página Sete</h1></body></html> o h1 caiu dentro de: BODY

Você entregou uma linha e recebeu três tags que nunca digitou. O parser de HTML é obrigado, pela especificação, a produzir uma árvore válida a partir de qualquer entrada — inclusive de uma entrada errada.

O erro mais comum: o navegador conserta a sua tag

Essa tolerância tem preço. Como o parser nunca reclama, o seu erro não vira mensagem: vira uma página silenciosamente diferente da que você imaginou.

js
const casos = {
  'p sem fechar': '<p>Livro do mês<p>Autor da semana',
  'div dentro de p': '<p>Promoção <div>50% off</div> até domingo</p>',
  'li sem fechar': '<ul><li>Machado de Assis<li>Clarice Lispector</ul>',
};

for (const [nome, html] of Object.entries(casos)) {
  const { document } = new JSDOM(html).window;
  console.log(`${nome}:`);
  console.log(`  entrou: ${html}`);
  console.log(`  virou:  ${document.body.innerHTML}`);
}
p sem fechar: entrou: <p>Livro do mês<p>Autor da semana virou: <p>Livro do mês</p><p>Autor da semana</p> div dentro de p: entrou: <p>Promoção <div>50% off</div> até domingo</p> virou: <p>Promoção </p><div>50% off</div> até domingo<p></p> li sem fechar: entrou: <ul><li>Machado de Assis<li>Clarice Lispector</ul> virou: <ul><li>Machado de Assis</li><li>Clarice Lispector</li></ul>

O primeiro e o terceiro caso deram certo por sorte: o parser fechou as tags exatamente onde você queria. O do meio é a armadilha. Você escreveu um parágrafo com uma div dentro; o navegador devolveu um parágrafo curto, uma div solta, um texto órfão e um parágrafo vazio no fim. Qualquer CSS escrito para “o parágrafo da promoção” vai pegar só a palavra Promoção, e você vai passar meia hora achando que o problema é o CSS.

Regra prática que evita quase todos esses casos: feche toda tag que você abriu, e nunca coloque um bloco (div, ul, h2) dentro de um p.

O acento que vira Café

Existe uma linha do head que não é opcional. Sem ela, o navegador precisa adivinhar em que codificação o arquivo foi salvo — e adivinha errado. Os dois arquivos a seguir têm exatamente os mesmos bytes no body, os dois gravados em UTF-8. Só o segundo avisa o navegador disso:

html
<!-- sem-charset.html -->
<!DOCTYPE html>
<html lang="pt-BR">
<head><title>Livraria</title></head>
<body><p>Café e leitura até as 22h</p></body>
</html>

<!-- com-charset.html: uma linha a mais, no head -->
<!DOCTYPE html>
<html lang="pt-BR">
<head><meta charset="UTF-8"><title>Livraria</title></head>
<body><p>Café e leitura até as 22h</p></body>
</html>
js
for (const arquivo of ['sem-charset.html', 'com-charset.html']) {
  const dom = await JSDOM.fromFile(arquivo);
  const { document } = dom.window;
  console.log(`${arquivo}`);
  console.log('  codificação adotada:', document.characterSet);
  console.log('  texto exibido:', document.querySelector('p').textContent);
}
sem-charset.html codificação adotada: windows-1252 texto exibido: Café e leitura até as 22h com-charset.html codificação adotada: UTF-8 texto exibido: Café e leitura até as 22h

Esse é o famoso acento quebrado. Repare que o palpite do arquivo sem meta foi windows-1252: sem a declaração, quem lê o arquivo escolhe uma codificação qualquer, e a escolha muda conforme o programa e o idioma do sistema. É por isso que a mesma página às vezes aparece certa na sua máquina e quebrada na do cliente. O diagnóstico completo, com o aviso que o Firefox mostra no console, está em the character encoding of the HTML document was not declared.

HTML, CSS e JavaScript: quem faz o quê

As três tecnologias trabalham sobre a mesma árvore, em papéis diferentes:

tecnologia responde a pergunta o que acontece se faltar
HTML o que é cada coisa não há página nenhuma
CSS qual a aparência de cada coisa a página fica com o visual padrão do navegador
JavaScript o que muda depois que a página abriu a página fica parada, mas funcionando

A ordem de aprendizado sai daí: HTML primeiro, porque os outros dois precisam de uma árvore para trabalhar. O JavaScript, por exemplo, encontra um nó pelo id e troca o conteúdo dele:

js
const html = `<!DOCTYPE html>
<html lang="pt-BR">
  <head><title>Livraria Página Sete</title></head>
  <body>
    <h1 id="vitrine">Livro do mês: Grande Sertão</h1>
    <script>
      const titulo = document.querySelector('#vitrine');
      titulo.textContent = 'Livro do mês: Vidas Secas';
    </script>
  </body>
</html>`;

const { document } = new JSDOM(html, { runScripts: 'dangerously' }).window;
console.log('o que o JavaScript deixou na tela:', document.querySelector('#vitrine').textContent);
o que o JavaScript deixou na tela: Livro do mês: Vidas Secas

O arquivo continua dizendo “Grande Sertão”. Quem mudou foi a árvore, em memória. Essa divisão de trabalho continua na trilha de CSS e em o que é JavaScript, as duas camadas que vêm depois desta.

Quem mais lê o seu HTML

O navegador não é o único leitor. Leitor de tela, robô de busca e programa de e-mail consomem a mesma árvore, e cada um extrai a parte que interessa a ele:

js
const { document } = new JSDOM(`
<h1>Livraria Página Sete</h1>
<img src="/fachada.jpg" alt="Fachada da livraria com toldo verde">
<img src="/estante.jpg">
<h2>Clube de leitura</h2>
<p>Encontro toda quinta. <a href="/clube">Ver a lista de livros</a>.</p>
<p><a href="/contato">Falar com a gente</a></p>`).window;

console.log('sumário de títulos:');
for (const t of document.querySelectorAll('h1,h2,h3')) {
  console.log(`  ${'-'.repeat(Number(t.tagName[1]))} ${t.textContent}`);
}
console.log('links da página:');
for (const a of document.querySelectorAll('a[href]')) {
  console.log(`  ${a.textContent} -> ${a.getAttribute('href')}`);
}
console.log('imagens:');
for (const img of document.querySelectorAll('img')) {
  console.log(`  ${img.getAttribute('src')} anunciada como: ${img.alt || '(sem alt: só o arquivo)'}`);
}
sumário de títulos: - Livraria Página Sete -- Clube de leitura links da página: Ver a lista de livros -> /clube Falar com a gente -> /contato imagens: /fachada.jpg anunciada como: Fachada da livraria com toldo verde /estante.jpg anunciada como: (sem alt: só o arquivo)

Esse é literalmente o menu que um leitor de tela oferece a quem não enxerga a página: pular de título em título, listar os links, ouvir a descrição das imagens. A imagem sem alt é a única que não tem o que anunciar. E o Google faz a mesma leitura para decidir do que a página trata.

O que você já consegue montar hoje

Com h1 a h6, p, ul/li e a, você já publica a página inteira da livraria: nome, endereço, horário, lista de eventos e link de contato. É pouca tag, e é o esqueleto de quase todo site que existe.

O próximo passo é entender o arquivo por inteiro, linha a linha, em estrutura de uma página HTML. Se preferir ver o mapa completo antes de continuar, o guia de HTML do zero mostra a ordem de estudo e onde cada assunto entra.

Prefere aprender em vídeo?

Tem aula sobre este assunto no nosso canal.

Ver todos os vídeos do canal
  • html
  • iniciante
  • marcacao
  • navegador

Perguntas frequentes

Qual a diferença entre HTML e HTML5?
Na prática, nenhuma que você precise decidir hoje. HTML5 foi o nome da versão que trouxe tags como header, article, video e os inputs de data e e-mail. Desde então a especificação virou um documento vivo, sem número de versão: o que você escreve hoje é só HTML.
Preciso instalar algum programa para escrever HTML?
Qualquer editor de texto puro serve, inclusive o Bloco de Notas. O que não serve é Word ou Google Docs, porque eles salvam formatação junto e o arquivo deixa de ser texto puro. Na prática quase todo mundo usa o VS Code, por causa do autocompletar de tag e da extensão Live Server.
Dá para publicar um site só com HTML, sem CSS e sem JavaScript?
Dá, e ele funciona. Fica com a aparência padrão do navegador, que é feia mas legível, e todos os links e formulários continuam operando. É assim que muita documentação técnica antiga ainda está no ar.
Por que o navegador abriu meu arquivo mostrando as tags como texto?
O arquivo foi salvo com a extensão errada, quase sempre .html.txt. O Windows esconde extensões conhecidas por padrão, então o nome parece certo no Explorador. Mande o editor salvar como "Todos os arquivos" e confira a extensão real antes de abrir.

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

Fontes consultadas

  1. MDN — Structuring content with HTML — developer.mozilla.org
  2. WHATWG — HTML Standard: Parsing HTML documents — html.spec.whatwg.org

Continue por aqui