Modifying Content
Three properties let you read or replace an element's content. Choose based on what you're setting and whether you trust the source.
| Property | Gets/Sets | Parses HTML? | Safe for user input? |
|---|---|---|---|
textContent | All text (including hidden) | No - escapes tags | Yes |
innerText | Visible text only | No - escapes tags | Yes |
innerHTML | HTML markup | Yes - creates elements | Never with untrusted input |
const box = document.querySelector('.box');
// textContent - plain text, HTML tags are escaped (not parsed)
box.textContent = 'Hello World';
// Renders: Hello <strong>World</strong> - shows the tag as text
// innerHTML - parses the string as HTML
box.innerHTML = 'Hello World';
// Renders: Hello **World** - bold "World"
// NEVER do this with user input - XSS vulnerability
const userInput = '<img src=x onerror="alert(1)">';
box.innerHTML = userInput; // DANGEROUS
// Safe alternative for user input
box.textContent = userInput; // Displays as literal text, no script executed
// Reading content
console.log(box.textContent); // 'Hello World' (no tags, just text)
console.log(box.innerHTML); // 'Hello <strong>World</strong>'
console.log(box.outerHTML); // '<div class="box">Hello <strong>World</strong></div>'
// Safe pattern: build with createElement, set text via textContent
function createUserCard(user) {
const card = document.createElement('div');
card.className = 'user-card';
const name = document.createElement('h3');
name.textContent = user.name; // safe - no XSS possible
const bio = document.createElement('p');
bio.textContent = user.bio; // safe even if bio contains HTML
card.append(name, bio);
return card;
}
// Using template literals with innerHTML - only for trusted/controlled data
function renderProductBadge(product) {
return `
<span class="badge">${product.category}</span>
<span class="price">$${product.price}</span>
`;
// OK only because product comes from your own API, not user input
}
Classes and Styles
classList is the recommended way to toggle appearance. Direct style manipulation should be a last resort.
const btn = document.querySelector('.btn');
// Add a class
btn.classList.add('active');
btn.classList.add('btn-primary', 'large'); // add multiple at once
// Remove a class
btn.classList.remove('disabled');
btn.classList.remove('btn-primary', 'large');
// Toggle - adds if absent, removes if present; returns true if added
const isActive = btn.classList.toggle('active');
console.log(isActive); // true if 'active' was just added
// Toggle with force: true=always add, false=always remove
btn.classList.toggle('hidden', !isLoggedIn); // hide when not logged in
// Check
console.log(btn.classList.contains('active')); // true or false
// Replace one class with another
btn.classList.replace('btn-primary', 'btn-danger');
// Read all classes
console.log([...btn.classList]); // ['btn', 'active', ...]
console.log(btn.className); // 'btn active' (space-separated string)
const box = document.querySelector('.box');
// Set inline styles - camelCase property names
box.style.backgroundColor = '#3b82f6';
box.style.padding = '1rem';
box.style.borderRadius = '8px';
// Remove an inline style by setting it to empty string
box.style.backgroundColor = '';
// Read computed style (not inline style) - includes CSS cascade
const computed = getComputedStyle(box);
console.log(computed.backgroundColor); // 'rgb(59, 130, 246)'
console.log(computed.fontSize); // '16px'
// cssText - replace all inline styles at once
box.style.cssText = 'color: red; font-size: 20px;';
// Prefer classList - define appearance in CSS, toggle classes in JS
// BAD: box.style.display = 'none';
// GOOD: box.classList.add('hidden'); -- .hidden { display: none; } in CSS
// Valid exception: dynamic values you can't express in CSS classes
function setProgress(percent) {
document.querySelector('.progress-bar').style.width = percent + '%';
}
Attributes and Data
const input = document.querySelector('input');
// setAttribute / getAttribute / removeAttribute / hasAttribute
input.setAttribute('placeholder', 'Enter email...');
input.setAttribute('disabled', ''); // boolean attribute
console.log(input.getAttribute('placeholder')); // 'Enter email...'
console.log(input.hasAttribute('disabled')); // true
input.removeAttribute('disabled');
// Direct property access (preferred for standard attributes)
input.type = 'email';
input.placeholder = 'Enter email...';
input.disabled = false;
// Note: .getAttribute('class') vs .className - both work but differ subtly
// data-* attributes via dataset
const card = document.querySelector('[data-user-id]');
// HTML: <div data-user-id="42" data-role="admin">
console.log(card.dataset.userId); // '42' (camelCase, always string)
console.log(card.dataset.role); // 'admin'
card.dataset.userId = '99'; // sets data-user-id="99"
card.dataset.score = 500; // sets data-score="500" (coerced to string)
delete card.dataset.score; // removes data-score attribute
// Parse numbers from dataset
const id = parseInt(card.dataset.userId, 10); // 99 as number
Inserting Elements
Modern DOM APIs give you precise control over where elements are inserted.
const list = document.querySelector('ul');
// append() - add to end, accepts nodes and strings
const li = document.createElement('li');
li.textContent = 'New item';
list.append(li);
list.append('Plain text node'); // string becomes a text node
// prepend() - add to beginning
list.prepend(li);
// before() / after() - insert relative to the element (not inside it)
const heading = document.querySelector('h2');
const note = document.createElement('p');
note.textContent = 'Read this first';
heading.before(note); // inserts note BEFORE the h2
heading.after(note); // moves note AFTER the h2
// insertAdjacentElement(position, element)
// Positions: 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
//
// <!-- beforebegin -->
// <div>
// <!-- afterbegin -->
// existing content
// <!-- beforeend -->
// </div>
// <!-- afterend -->
const sidebar = document.querySelector('.sidebar');
sidebar.insertAdjacentElement('afterbegin', note); // first child inside sidebar
// insertAdjacentHTML - for strings of HTML (trusted content only)
sidebar.insertAdjacentHTML('beforeend', '<p class="footer-note">Updated</p>');
// replaceWith() - replace an element with another
const oldBtn = document.querySelector('.old-btn');
const newBtn = document.createElement('button');
newBtn.textContent = 'New Button';
oldBtn.replaceWith(newBtn);
When inserting many elements in a loop, add them to a DocumentFragment first, then insert the fragment once. This triggers only one reflow instead of one per element - a significant performance gain for large lists.
const frag = document.createDocumentFragment();
items.forEach(item => {
const li = document.createElement("li");
li.textContent = item;
frag.append(li);
});
list.append(frag); // single DOM updateRemoving Elements
// remove() - modern, call on the element itself
document.querySelector('.modal').remove();
// removeChild() - older, requires parent reference
const parent = document.querySelector('.list');
const child = parent.querySelector('.item');
parent.removeChild(child);
// Remove all children - fastest way to clear a container
const container = document.querySelector('.gallery');
container.innerHTML = ''; // simple but destroys event listeners on children
// Alternative that keeps references (useful if children have listeners):
while (container.firstChild) {
container.removeChild(container.firstChild);
}
// Or modern replaceChildren() with no args:
container.replaceChildren();
// Detach and re-attach (temporarily remove for batch updates)
const table = document.querySelector('table');
const parent2 = table.parentNode;
const nextSib = table.nextSibling;
table.remove(); // detach from DOM
// ... make many DOM changes to table ...
parent2.insertBefore(table, nextSib); // re-attach
// Remove elements matching a condition
document.querySelectorAll('.error-message').forEach(el => el.remove());
Cloning Nodes
// cloneNode(deep)
// deep=true - clone element AND all its descendants
// deep=false - clone only the element itself, no children
const original = document.querySelector('.card');
const shallowCopy = original.cloneNode(false); // empty clone, no children
const deepCopy = original.cloneNode(true); // full copy with all children
// Clone does NOT copy event listeners - re-attach them
deepCopy.querySelector('button').addEventListener('click', handleClick);
document.querySelector('.card-grid').append(deepCopy);
// Practical: card list from a template
const template = document.querySelector('#card-template'); // <div class="card">...</div>
function createCard(data) {
const card = template.cloneNode(true);
card.removeAttribute('id'); // remove id to avoid duplicates
card.querySelector('.card-title').textContent = data.title;
card.querySelector('.card-body').textContent = data.body;
card.dataset.id = data.id;
return card;
}
const frag = document.createDocumentFragment();
products.forEach(p => frag.append(createCard(p)));
document.querySelector('.card-grid').append(frag);
// <template> element - best practice for HTML templates
// Content inside <template> is inert (not rendered, not fetched)
const tmpl = document.querySelector('template');
const clone = tmpl.content.cloneNode(true); // clones the template content
document.body.append(clone);
The HTML <template> element is purpose-built for reusable markup. Its content is a DocumentFragment - not rendered, images not loaded, scripts not executed until you clone and insert it. Use it instead of hidden elements for cloning patterns.
Working with Multiple Elements
querySelectorAll returns a static NodeList. Convert to an array to use all array methods.
// NodeList to Array
const items = Array.from(document.querySelectorAll('.item'));
// or: const items = [...document.querySelectorAll('.item')];
// forEach is available on NodeList directly (no conversion needed)
document.querySelectorAll('.card').forEach(card => {
card.classList.add('loaded');
});
// But map/filter/reduce require an array
const titles = Array.from(document.querySelectorAll('h2'))
.map(h2 => h2.textContent);
// Filter elements by a condition
const errorInputs = Array.from(document.querySelectorAll('input'))
.filter(input => input.value.trim() === '');
// Find the first matching element in a set
const firstActive = Array.from(document.querySelectorAll('.tab'))
.find(tab => tab.classList.contains('active'));
// Apply changes to a filtered subset
Array.from(document.querySelectorAll('.price'))
.filter(el => parseFloat(el.dataset.amount) > 100)
.forEach(el => el.classList.add('premium'));
// Collect values from inputs
const values = Array.from(document.querySelectorAll('input[name="tag"]'))
.map(input => input.value)
.filter(Boolean); // remove empty strings
Practical DOM Patterns
// Pattern 1: Toggle visibility
function toggleMenu(menuEl) {
menuEl.classList.toggle('is-open');
menuEl.setAttribute('aria-expanded',
menuEl.classList.contains('is-open').toString()
);
}
// Pattern 2: Show/hide with animation (add class, wait for transition)
function dismissAlert(alertEl) {
alertEl.classList.add('fade-out');
alertEl.addEventListener('transitionend', () => alertEl.remove(), { once: true });
}
// Pattern 3: Render a list from data
function renderList(container, items, renderFn) {
container.replaceChildren(); // clear existing
if (items.length === 0) {
container.append('No items found.');
return;
}
const frag = document.createDocumentFragment();
items.forEach(item => frag.append(renderFn(item)));
container.append(frag);
}
// Pattern 4: Highlight active nav link
function setActiveLink(links, activeHref) {
links.forEach(link => {
link.classList.toggle('active', link.href === activeHref);
link.setAttribute('aria-current',
link.href === activeHref ? 'page' : 'false'
);
});
}
// Pattern 5: Lazy-load attribute swap
document.querySelectorAll('img[data-src]').forEach(img => {
img.src = img.dataset.src;
img.removeAttribute('data-src');
});
Reading offsetWidth, getBoundingClientRect(), or any property that forces the browser to recalculate layout inside a write loop causes layout thrashing. Batch reads before writes: read all values first, then apply all DOM changes.