Manipulação de DOM é criar, inserir, mover, modificar e remover elementos. As APIs modernas (append, prepend, insertAdjacentHTML, replaceWith, remove) são mais ergonômicas que as legadas (appendChild, insertBefore, removeChild). A distinção crítica: innerHTML re-parseia HTML e é vetor de XSS quando o conteúdo vem do usuário; textContent é seguro e define texto puro; innerText considera estilo (mais lento, use menos).
Criar elementos
// Criar um elementoconst div = document.createElement('div');const img = document.createElement('img');const text = document.createTextNode('Olá');// Criar com atributos imediatamenteconst link = document.createElement('a');link.href = 'https://exemplo.com';link.textContent = 'Clique aqui';link.className = 'link--external';link.target = '_blank';link.rel = 'noopener noreferrer'; // segurança// Criar via innerHTML (cria subárvore — cuidado com XSS)const wrapper = document.createElement('div');wrapper.innerHTML = '<p class="texto">Conteúdo <strong>negrito</strong></p>';// Só é seguro quando o conteúdo é seu próprio código, não dado de usuário
Inserir elementos — API moderna
const parent = document.querySelector('.container');const newEl = document.createElement('p');newEl.textContent = 'Novo parágrafo';// append: no FIM do pai (aceita múltiplos, aceita strings)parent.append(newEl);parent.append('texto puro', document.createElement('br'), newEl2);// prepend: no INÍCIO do paiparent.prepend(newEl);// before / after: antes/depois do elemento (como irmão)const existing = document.querySelector('.existing');existing.before(newEl); // newEl fica antes de .existingexisting.after(newEl); // newEl fica depois de .existing// replaceWith: substituir o elementoexisting.replaceWith(newEl);
insertAdjacentHTML — inserir HTML em posições específicas
const el = document.querySelector('.card');// Posições possíveis:el.insertAdjacentHTML('beforebegin', '<div>antes do card</div>');// ↳ [AQUI]<div class="card">...</div>el.insertAdjacentHTML('afterbegin', '<header>início do card</header>');// ↳ <div class="card">[AQUI]...</div>el.insertAdjacentHTML('beforeend', '<footer>fim do card</footer>');// ↳ <div class="card">...[AQUI]</div>el.insertAdjacentHTML('afterend', '<div>após o card</div>');// ↳ <div class="card">...</div>[AQUI]// insertAdjacentElement: mesmo mas com Element (não string)el.insertAdjacentElement('afterend', document.createElement('hr'));// insertAdjacentText: texto puro (seguro para user input)el.insertAdjacentText('beforeend', userInputText);
APIs legadas — ainda comuns em código existente
// appendChild: append um filho (só um Element, não string)parent.appendChild(newEl);// insertBefore: inserir antes de um filho referênciaparent.insertBefore(newEl, referenceEl);// Equivalente moderno: referenceEl.before(newEl)// removeChild: remover um filho específicoparent.removeChild(childEl);// Equivalente moderno: childEl.remove()// replaceChild: substituir filhoparent.replaceChild(newEl, oldEl);// Equivalente moderno: oldEl.replaceWith(newEl)
Remover e mover elementos
const el = document.querySelector('.removable');// Remover da árvoreel.remove();// Mover (inserir em outro lugar — remove do antigo automaticamente)const target = document.querySelector('.new-parent');target.append(el); // el é removido de onde estava e inserido aqui// Limpar todos os filhosparent.replaceChildren(); // sem argumentos — remove tudoparent.innerHTML = ''; // alternativa (mas re-parseia)
innerHTML vs textContent vs innerText
Esta é uma das distinções mais importantes para performance e segurança:
Propriedade
Lê
Escreve
Parseia HTML
Considera CSS
Seguro para user input
innerHTML
HTML completo
Re-parseia HTML
Sim
Não
❌ XSS
textContent
Texto bruto de todos os filhos
Substitui conteúdo como texto
Não
Não
✅
innerText
Texto visível (como renderizado)
Substitui como texto
Não
Sim (slow)
✅
outerHTML
HTML do elemento + filhos
Substitui o próprio elemento
Sim
Não
❌ XSS
const el = document.querySelector('.output');// textContent — SEMPRE para output de dados do usuárioel.textContent = userInput; // tags HTML ficam como texto literal (seguro)// innerHTML — APENAS para HTML de strings template de código seuel.innerHTML = `<strong>${formatName(user.name)}</strong>`; // OK se você controla formatNameel.innerHTML = userInput; // ❌ XSS: se userInput = '<img src=x onerror=alert(1)>'// innerText — evite; é lento porque força layout (precisa saber o que é visível)el.innerText = text; // OK mas use textContent quando possível
innerHTML e XSS
Nunca insira dados de usuário, URL params, ou conteúdo externo via innerHTML sem sanitização. Use textContent para texto, ou bibliotecas como DOMPurify para HTML que precisa ser renderizado:
import DOMPurify from 'dompurify';el.innerHTML = DOMPurify.sanitize(untrustedHTML);
Modificar conteúdo existente
const el = document.querySelector('.card');// Ler e escrever textoconst text = el.textContent; // todo texto incluindo descendentesel.textContent = 'Novo conteúdo'; // substitui TODOS os filhos por texto// Ler e escrever HTMLconst html = el.innerHTML; // HTML interno como stringel.innerHTML = '<p>Novo HTML</p>'; // substitui filhos (re-parseia)// Ler e escrever o elemento em si (inclusive)const outerHtml = el.outerHTML;el.outerHTML = '<div class="novo">...</div>'; // substitui o próprio elemento
replaceChildren — substituir todos os filhos
const list = document.querySelector('ul');// Substituir todos os filhos de uma vez (limpo e eficiente)const items = data.map(item => { const li = document.createElement('li'); li.textContent = item.name; return li;});list.replaceChildren(...items);// Equivalente a: list.innerHTML = '' + append de cada item// Mas mais eficiente e sem re-parse
Mover elemento sem perder event listeners
Um elemento que é append-ado é movido — não copiado. Seus event listeners se mantêm:
const el = document.querySelector('.widget');el.addEventListener('click', handler); // listener registrado// Mover para outro containerdocument.querySelector('.other-container').append(el);// O listener de 'click' continua ativo no elemento movido// Para COPIAR sem listeners:const clone = el.cloneNode(false); // false = sem filhosconst deepClone = el.cloneNode(true); // true = com filhos// cloneNode NÃO copia event listeners
Para fixar
Qual a diferença entre el.append('texto') e el.appendChild(document.createTextNode('texto'))?
Por que innerHTML = userInput é um vetor de XSS? Dê um exemplo de payload malicioso e como a exploração ocorreria.
Qual a diferença entre textContent e innerText? Quando innerText retorna algo diferente de textContent?
O que insertAdjacentHTML('afterend', html) faz de diferente de el.after(novoEl)?
Um elemento tem 3 event listeners registrados. Você move ele para outro container com append() — os listeners são preservados? E se você usar cloneNode(true)?