Até agora todo o nosso código rodou no console — um ambiente controlado, sem interface visual. A partir deste módulo mudamos completamente de cenário. Vamos aprender a manipular páginas web de verdade, reagir a cliques, criar e remover elementos, validar formulários e dar vida a interfaces.
Para isso, precisamos primeiro entender o DOM — o conceito central de todo desenvolvimento front-end com JavaScript.
O que é o DOM?
DOM significa Document Object Model — Modelo de Objeto do Documento. Quando o navegador carrega um arquivo HTML, ele não enxerga texto. Ele parseia (interpreta) esse texto e constrói uma representação em memória da página — uma árvore de objetos que o JavaScript pode ler e modificar.
Considere este HTML simples:
<!DOCTYPE html>
<html>
<head>
<title>Minha Página</title>
</head>
<body>
<h1 id="titulo">Olá, mundo!</h1>
<p class="descricao">Bem-vindo ao curso.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</body>
</html>
O navegador transforma isso em uma árvore de nós:
document
└── html
├── head
│ └── title → "Minha Página"
└── body
├── h1#titulo → "Olá, mundo!"
├── p.descricao → "Bem-vindo ao curso."
└── ul
├── li → "Item 1"
├── li → "Item 2"
└── li → "Item 3"
Cada elemento HTML vira um nó nessa árvore. E o JavaScript pode acessar, modificar, criar e remover qualquer nó dessa árvore em tempo real — sem recarregar a página.
O objeto document
O ponto de entrada para o DOM é o objeto global document. Ele representa a página inteira e é o ponto de partida para qualquer manipulação:
console.log(document); // o documento inteiro
console.log(document.title); // "Minha Página"
console.log(document.URL); // URL da página atual
console.log(document.body); // o elemento <body>
console.log(document.head); // o elemento <head>
console.log(document.documentElement); // o elemento <html>
Selecionando elementos
Para manipular um elemento, primeiro você precisa selecioná-lo. Existem vários métodos:
getElementById
Seleciona um único elemento pelo seu id:
const titulo = document.getElementById("titulo");
console.log(titulo); // <h1 id="titulo">Olá, mundo!</h1>
console.log(titulo.textContent); // "Olá, mundo!"
getElementsByClassName
Seleciona todos os elementos com uma determinada classe — retorna uma HTMLCollection (parecida com array, mas não é):
const descricoes = document.getElementsByClassName("descricao");
console.log(descricoes[0].textContent); // "Bem-vindo ao curso."
getElementsByTagName
Seleciona todos os elementos de uma tag:
const itens = document.getElementsByTagName("li");
console.log(itens.length); // 3
querySelector e querySelectorAll — os modernos
Estes dois métodos usam seletores CSS e são os mais usados hoje em dia:
// querySelector — retorna o PRIMEIRO elemento que corresponde
const titulo = document.querySelector("#titulo");
const descricao = document.querySelector(".descricao");
const primeiroItem = document.querySelector("li");
const inputEmail = document.querySelector("input[type='email']");
// querySelectorAll — retorna TODOS os elementos correspondentes (NodeList)
const todosItens = document.querySelectorAll("li");
const botoesAtivos = document.querySelectorAll(".btn.ativo");
console.log(todosItens.length); // 3
// NodeList pode ser percorrida com forEach
todosItens.forEach(item => {
console.log(item.textContent);
});
// Item 1
// Item 2
// Item 3
Na prática moderna, use quase exclusivamente querySelector e querySelectorAll. Eles são flexíveis, consistentes e usam a mesma sintaxe do CSS que você já conhece.
Lendo e modificando conteúdo
Com o elemento selecionado, você pode ler e modificar seu conteúdo:
const titulo = document.querySelector("#titulo");
// Lendo conteúdo
console.log(titulo.textContent); // "Olá, mundo!" — texto puro
console.log(titulo.innerHTML); // "Olá, mundo!" — HTML interno
// Modificando conteúdo
titulo.textContent = "Bem-vindo ao JavaScript!";
// innerHTML interpreta tags HTML
titulo.innerHTML = "Bem-vindo ao <strong>JavaScript</strong>!";
Atenção: use textContent quando estiver inserindo texto simples. Use innerHTML apenas quando precisar inserir HTML — e nunca com dados vindos do usuário, pois isso abre vulnerabilidades de segurança (XSS).
Lendo e modificando atributos
const link = document.querySelector("a");
// Lendo atributos
console.log(link.getAttribute("href")); // "https://..."
console.log(link.id);
console.log(link.className);
// Modificando atributos
link.setAttribute("href", "https://developer.mozilla.org");
link.setAttribute("target", "_blank");
// Verificando se existe
console.log(link.hasAttribute("href")); // true
// Removendo
link.removeAttribute("target");
Manipulando estilos CSS
const titulo = document.querySelector("#titulo");
// Modificando estilos inline
titulo.style.color = "royalblue";
titulo.style.fontSize = "2.5rem";
titulo.style.fontWeight = "bold";
titulo.style.backgroundColor = "#f0f4ff";
titulo.style.padding = "1rem";
Mas modificar estilos diretamente com JavaScript é considerado má prática para a maioria dos casos. A forma profissional é manipular classes CSS:
Manipulando classes CSS — classList
O classList é uma das APIs mais usadas no desenvolvimento front-end:
const botao = document.querySelector("#meuBotao");
// Adicionar classe
botao.classList.add("ativo");
botao.classList.add("destacado", "grande"); // múltiplas de uma vez
// Remover classe
botao.classList.remove("destacado");
// Alternar (adiciona se não tem, remove se tem)
botao.classList.toggle("ativo");
// Verificar se tem uma classe
console.log(botao.classList.contains("ativo")); // true ou false
// Substituir uma classe por outra
botao.classList.replace("grande", "pequeno");
Este padrão é o correto: você define os estilos no CSS e usa o JavaScript apenas para adicionar ou remover classes:
/* No CSS */
.ativo {
background-color: royalblue;
color: white;
}
.oculto {
display: none;
}
// No JavaScript — apenas gerencia as classes
botao.classList.toggle("ativo");
modal.classList.add("oculto");
Percorrendo a árvore do DOM
O DOM é uma árvore e você pode navegar entre os nós:
const lista = document.querySelector("ul");
console.log(lista.parentElement); // elemento pai — <body>
console.log(lista.children); // filhos diretos — HTMLCollection de <li>s
console.log(lista.firstElementChild); // primeiro filho — primeiro <li>
console.log(lista.lastElementChild); // último filho — último <li>
const primeiroItem = lista.firstElementChild;
console.log(primeiroItem.nextElementSibling); // segundo <li>
console.log(primeiroItem.previousElementSibling); // null (é o primeiro)
Um exemplo completo
Vamos criar uma pequena página e manipulá-la com JavaScript:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>DOM em ação</title>
<style>
.destaque { color: royalblue; font-weight: bold; }
.oculto { display: none; }
.concluido { text-decoration: line-through; color: gray; }
</style>
</head>
<body>
<h1 id="titulo">Lista de Tarefas</h1>
<p id="contador">0 tarefas</p>
<ul id="lista">
<li>Estudar JavaScript</li>
<li>Praticar no console</li>
<li>Construir um projeto</li>
</ul>
<div id="info" class="oculto">
Você passou o mouse em: <span id="itemAtual"></span>
</div>
<script>
// Selecionar elementos
const titulo = document.querySelector("#titulo");
const contador = document.querySelector("#contador");
const lista = document.querySelector("#lista");
const info = document.querySelector("#info");
const itemAtual = document.querySelector("#itemAtual");
const itens = document.querySelectorAll("li");
// Atualizar o título
titulo.classList.add("destaque");
// Atualizar contador
contador.textContent = `${itens.length} tarefas`;
// Marcar primeiro item como concluído
lista.firstElementChild.classList.add("concluido");
// Interagir com cada item
itens.forEach(item => {
// Ao passar o mouse — mostra info
item.addEventListener("mouseenter", () => {
itemAtual.textContent = item.textContent;
info.classList.remove("oculto");
});
// Ao tirar o mouse — oculta info
item.addEventListener("mouseleave", () => {
info.classList.add("oculto");
});
// Ao clicar — alterna concluído
item.addEventListener("click", () => {
item.classList.toggle("concluido");
});
});
</script>
</body>
</html>
Salve como .html, abra no navegador e observe o DOM funcionando em tempo real. Veja como o JavaScript e o CSS trabalham juntos — JS gerencia as classes, CSS cuida da aparência.
DevTools — sua ferramenta de trabalho
Pressione F12 no navegador, vá na aba Elements (ou Elementos). Você verá a árvore do DOM ao vivo. Clique em qualquer elemento e no console digite:
// $0 sempre referencia o elemento selecionado no DevTools
console.log($0);
$0.style.border = "2px solid red";
$0.classList.add("destaque");
O DevTools é seu laboratório. Experimente tudo por lá antes de escrever no código.
Boas práticas ao trabalhar com DOM
// ✅ 1. Sempre verifique se o elemento existe antes de manipulá-lo
const botao = document.querySelector("#botaoEnviar");
if (botao) {
botao.classList.add("ativo");
}
// ✅ 2. Cache seus seletores — não selecione o mesmo elemento várias vezes
// ❌ Ruim — seleciona 3 vezes
document.querySelector("#titulo").textContent = "Novo título";
document.querySelector("#titulo").classList.add("destaque");
document.querySelector("#titulo").style.display = "block";
// ✅ Bom — seleciona uma vez e reutiliza
const titulo = document.querySelector("#titulo");
titulo.textContent = "Novo título";
titulo.classList.add("destaque");
titulo.style.display = "block";
// ✅ 3. Prefira classList a style para manipular aparência
// ❌ Evite
elemento.style.color = "blue";
elemento.style.fontWeight = "bold";
// ✅ Prefira
elemento.classList.add("destaque");
// ✅ 4. Use textContent para texto simples — nunca innerHTML com dados do usuário
const input = document.querySelector("#busca");
const resultado = document.querySelector("#resultado");
// ❌ Vulnerável a XSS
resultado.innerHTML = input.value;
// ✅ Seguro
resultado.textContent = input.value;
Tarefa para você
Crie um arquivo HTML com a seguinte estrutura e manipule-o com JavaScript:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>Exercício DOM</title>
</head>
<body>
<h1 id="titulo">Meu Perfil</h1>
<img id="avatar" src="https://via.placeholder.com/100" alt="Avatar">
<p id="nome">Nome: ???</p>
<p id="profissao">Profissão: ???</p>
<ul id="habilidades"></ul>
<script>
// 1. Altere o título para seu nome
// 2. Altere os parágrafos com seus dados reais
// 3. Adicione 3 habilidades à lista dinamicamente
// (crie elementos <li> com JavaScript)
// 4. Aplique uma cor de destaque ao título via classList
// 5. Altere o src do avatar para uma imagem real
</script>
</body>
</html>
Ver solução — as cinco tarefas do script, com o DOM certo para cada uma
// Cole no lugar do <script> do exercício. A página é a mesma do
// enunciado: #titulo, #avatar, #nome, #profissao e a <ul> #habilidades.
// ---------------------------------------------------------------
// 1 — o título
// ---------------------------------------------------------------
const titulo = document.querySelector("#titulo");
titulo.textContent = "Ana Prado";
// textContent, não innerHTML: o nome é TEXTO. Se viesse de um campo
// preenchido pelo usuário, innerHTML executaria o que houvesse ali —
// é assim que nasce um XSS.
// titulo.innerHTML = nomeDigitado; // ❌ nunca com dado de fora
// ---------------------------------------------------------------
// 2 — os parágrafos
// ---------------------------------------------------------------
const perfil = {
nome: "Ana Prado",
profissao: "Desenvolvedora front-end",
habilidades: ["JavaScript", "CSS", "Acessibilidade"],
avatar: "https://avatars.githubusercontent.com/u/9919?s=200",
};
document.querySelector("#nome").textContent = `Nome: ${perfil.nome}`;
document.querySelector("#profissao").textContent = `Profissão: ${perfil.profissao}`;
// ---------------------------------------------------------------
// 3 — as habilidades, criadas do zero
// ---------------------------------------------------------------
const listaHabilidades = document.querySelector("#habilidades");
// O fragment junta os três <li> na memória e encosta no documento uma
// vez só. Com três itens tanto faz; com trezentos, é a diferença entre
// um reflow e trezentos.
const fragmento = document.createDocumentFragment();
for (const habilidade of perfil.habilidades) {
const item = document.createElement("li");
item.textContent = habilidade;
item.classList.add("habilidade");
fragmento.appendChild(item);
}
listaHabilidades.appendChild(fragmento);
// ---------------------------------------------------------------
// 4 — a cor de destaque, via classList
// ---------------------------------------------------------------
// classList em vez de style.color: a decisão de COMO fica bonito é do
// CSS. O JavaScript só diz em que estado o elemento está.
titulo.classList.add("destaque");
// A folha de estilo (no <head> da página):
// .destaque { color: #5c6bc0; border-bottom: 3px solid #5c6bc0;
// display: inline-block; padding-bottom: .25rem; }
// .habilidade { display: inline-block; background: #eef1ff;
// border-radius: 999px; padding: .2rem .8rem;
// margin: .15rem; font-size: .9rem; }
// Os quatro métodos que importam:
titulo.classList.add("destaque"); // adiciona (não duplica)
titulo.classList.remove("inexistente"); // remove (não reclama se não tem)
titulo.classList.toggle("destaque"); // liga/desliga — aqui DESLIGA
titulo.classList.toggle("destaque"); // e liga de novo
console.log(titulo.classList.contains("destaque")); // true
// ---------------------------------------------------------------
// 5 — o avatar
// ---------------------------------------------------------------
const avatar = document.querySelector("#avatar");
avatar.src = perfil.avatar;
avatar.alt = `Foto de ${perfil.nome}`; // alt junto: imagem sem alt é imagem invisível
// Imagem externa falha — e sem tratamento a página fica com o ícone
// quebrado. O evento error dá a chance de cair num placeholder.
avatar.addEventListener("error", () => {
avatar.src = "https://via.placeholder.com/100?text=sem+foto";
avatar.alt = "Avatar indisponível";
});
// ---------------------------------------------------------------
// O detalhe que morde: o script tem de rodar DEPOIS do HTML existir
// ---------------------------------------------------------------
// No exercício o <script> está no fim do <body>, então tudo acima já
// existe quando ele roda. Movido para o <head>, todo querySelector
// devolveria null e a primeira atribuição estouraria:
//
// TypeError: Cannot set properties of null (setting 'textContent')
//
// As duas saídas:
// <script defer src="app.js"></script> ← no <head>, roda após o parse
// document.addEventListener("DOMContentLoaded", () => { ... });
querySelector devolve null quando não acha — não lança erro. O erro só aparece uma linha depois, ao mexer no null, e a mensagem aponta para a linha errada. Se o seletor está certo e o retorno é null, a causa quase sempre é ordem: o script rodou antes de o elemento existir.
O DOM não é o HTML: é a árvore de objetos que o navegador constrói a partir dele, e essa diferença é o que explica por que alterar a página pelo JavaScript não muda o arquivo no servidor. Para selecionar, querySelector e querySelectorAll dão conta de quase tudo — os métodos mais antigos continuam existindo, mas raramente compensam. E entre textContent e innerHTML a escolha é de segurança antes de ser de gosto: o primeiro trata o que recebe como texto, o segundo interpreta como marcação.
Fontes e Referências
- MDN Web Docs — Introduction to the DOM: https://developer.mozilla.org/pt-BR/docs/Web/API/Document_Object_Model/Introduction
- MDN Web Docs — Document: https://developer.mozilla.org/pt-BR/docs/Web/API/Document
- MDN Web Docs — Element.classList: https://developer.mozilla.org/pt-BR/docs/Web/API/Element/classList
- MDN Web Docs — querySelector: https://developer.mozilla.org/pt-BR/docs/Web/API/Document/querySelector
- JavaScript.info — Document: https://javascript.info/document
- JavaScript.info — DOM Tree: https://javascript.info/dom-nodes
- Eloquent JavaScript, Cap. 14 — The Document Object Model: https://eloquentjavascript.net/14_dom.html
- JavaScript & JQuery — Jon Duckett (Alta Books)
Exercícios
Exercício 1
Com a lista abaixo na página, o que cada linha marcada devolve — e por que a última quebra?
<ul id="lista">
<li class="item">A</li>
<li class="item">B</li>
<li class="item">C</li>
</ul>
const vivos = document.getElementsByClassName("item");
const estaticos = document.querySelectorAll(".item");
document.querySelector("#lista").firstElementChild.remove();
console.log(vivos.length); // A
console.log(estaticos.length); // B
estaticos.forEach(li => console.log(li.textContent)); // C
vivos.forEach(li => console.log(li.textContent)); // D
Ver resposta
✓ Resposta: A é 2 e B é 3. A HTMLCollection de getElementsByClassName é viva: ela não guarda elementos, guarda uma consulta, e reflete o documento no instante em que você a consulta. Já a NodeList de querySelectorAll é um retrato tirado na hora da chamada e não muda mais. C imprime A, B e C — o <li> removido saiu do documento, mas continua existindo como objeto na memória, e a NodeList ainda o segura. D lança TypeError: vivos.forEach is not a function: NodeList ganhou forEach, HTMLCollection não. Para percorrê-la, Array.from(vivos) ou [...vivos] — e a conversão, de quebra, congela a coleção, o que evita o clássico laço que remove elementos e pula a metade deles porque a lista encolhe embaixo dele.
Exercício 2
O usuário digitou o texto abaixo num campo de busca. Descreva o que aparece na tela em cada uma das duas linhas.
const alvo = document.querySelector("#resultado");
const digitado = '<img src="x" onerror="roubarSessao()">';
alvo.textContent = digitado; // A
alvo.innerHTML = digitado; // B
Ver resposta
✓ Resposta: Em A aparece na tela, literalmente, o texto <img src="x" onerror="roubarSessao()">. O textContent não interpreta nada: ele escreve caracteres, e o navegador escapa os sinais de menor e maior sozinho. Em B não aparece imagem nenhuma visível — e é justamente esse o problema. O innerHTML manda o navegador parsear a string, o que cria um elemento <img> de verdade apontando para x, que não existe; o carregamento falha, o navegador dispara o evento error e executa roubarSessao(). Isso é um XSS, e ele não precisa de <script> para acontecer — filtrar a palavra "script" da entrada não protege nada. A regra prática é a do artigo, sem exceção: dado que veio de fora entra por textContent. O innerHTML fica para HTML que você escreveu.
Exercício 3
A regra .destaque { color: royalblue; } está na folha de estilo. O que sai em cada linha?
const titulo = document.querySelector("#titulo");
titulo.classList.add("destaque");
console.log(titulo.style.color); // A
console.log(getComputedStyle(titulo).color); // B
titulo.style.color = "red";
console.log(titulo.classList.contains("destaque")); // C
Ver resposta
✓ Resposta: A é a string vazia "". A propriedade style é uma janela para o atributo style="" da tag e nada mais — ela não sabe nada de folha de estilo, e a cor veio da classe. B é "rgb(65, 105, 225)": o getComputedStyle resolve a cascata inteira e sempre devolve o valor já computado, em rgb(), nunca o nome que você escreveu. C é true — atribuir um estilo inline não remove classe alguma; ele apenas passa na frente na cascata. E é aí que mora o incômodo de misturar os dois: uma vez que existe estilo inline, a classe só volta a mandar com !important, e o jeito limpo de desfazer é titulo.style.color = "", que apaga a declaração inline e devolve o comando ao CSS.
Exercício 4
O HTML está indentado, como todo HTML escrito por gente. Quanto dá cada uma das quatro linhas?
<ul id="lista">
<li>Item 1</li>
<li>Item 2</li>
</ul>
const lista = document.querySelector("#lista");
console.log(lista.children.length); // A
console.log(lista.childNodes.length); // B
console.log(lista.firstChild); // C
console.log(lista.firstElementChild); // D
Ver resposta
✓ Resposta: A é 2 e B é 5. O children conta só elementos; o childNodes conta todos os nós, e a quebra de linha com a indentação entre as tags é um nó de texto legítimo — há um antes do primeiro <li>, um entre os dois e um depois do segundo, somando três nós de texto mais os dois elementos. Por isso C não é o <li>: é um #text com uma quebra de linha e dois espaços dentro. D é o <li>Item 1</li> que você esperava. É exatamente para isso que existem as variantes com Element no nome — firstElementChild, lastElementChild, nextElementSibling —: elas pulam texto e comentário. O detalhe traiçoeiro é que, se o HTML vier minificado numa linha só, childNodes.length passa a valer 2 e firstChild acerta o <li> — o bug some no build de produção e volta em desenvolvimento.
Exercício 5
Este é o contador do exemplo completo do artigo. Depois que um sexto <li> entra na lista via JavaScript, o parágrafo passa a mostrar 6? E se a seleção tivesse sido feita com getElementsByTagName, que devolve coleção viva?
const itens = document.querySelectorAll("li");
contador.textContent = `${itens.length} tarefas`; // "5 tarefas"
// mais tarde, em outro ponto do código:
lista.appendChild(document.createElement("li"));
Ver resposta
✓ Resposta: Não, nas duas versões — e por motivos diferentes, o que torna a pergunta útil. Com querySelectorAll, itens é um retrato estático: itens.length continua 5 para sempre. Com getElementsByTagName, itens.length de fato passa a devolver 6 na próxima vez que for lido — mas o parágrafo continua escrito "5 tarefas", porque o texto foi gravado uma vez, com o valor daquele instante, e não existe nenhum vínculo entre a coleção e a string que já está na tela. "Viva" descreve a coleção, nunca o DOM que você já escreveu. A correção é sempre a mesma: extrair uma função atualizarContador() e chamá-la depois de toda alteração na lista. Reconhecer esse trabalho repetitivo — mudou o dado, agora não esqueça de repintar a tela — é reconhecer o problema que React, Vue e companhia existem para resolver.