The Complete Guide to JavaScript DOM Manipulation
DOM manipulation is how you make web pages interactive. Here's everything you need to know.
What is the DOM?
HTML (text) → Browser parses it → DOM (tree of objects) → You manipulate it
DOM = Document Object Model
= Your HTML as a JavaScript object tree
= Every HTML tag becomes a JS object you can read/modify
<!-- This HTML: -->
<div id="app">
<h1 class="title">Hello</h1>
<p data-id="1">First paragraph</p>
<p data-id="2">Second paragraph</p>
</div>
<!-- Becomes this DOM tree:
Document
└── html
├── head
└── body
└── div#app
├── h1.title → "Hello"
├── p[data-id="1"] → "First paragraph"
└── p[data-id="2"] → "Second paragraph"
-->
Selecting Elements
// Single element selectors (returns ONE element or null)
document.getElementById('app'); // By ID (fastest)
document.querySelector('.title'); // First matching CSS selector
document.querySelector('p[data-id="1"]'); // Complex CSS selector
// Multiple element selectors (returns NodeList/HTMLCollection)
document.getElementsByClassName('title'); // Live collection (changes with DOM)
document.getElementsByTagName('p'); // Live collection
document.querySelectorAll('p'); // Static snapshot (doesn't update)
// Best practice: use querySelectorAll for consistency
const items = document.querySelectorAll('.item');
items.forEach(item => console.log(item));
// Note: querySelectorAll returns NodeList, NOT array!
// Convert to array if needed:
Array.from(items);
[...items]; // Spread syntax
Reading & Modifying Content
// Text content (no HTML)
el.textContent = 'Hello'; // Set text
console.log(el.textContent); // Get text (includes all nested text)
// Inner HTML (parses HTML)
el.innerHTML = '<strong>Bold</strong> text';
// ⚠️ Security risk with user input! Use textContent for user data.
// Outer HTML (includes the element itself)
console.log(el.outerHTML);
// Value (for form elements)
input.value = 'new value';
select.value = 'option2';
textarea.value = 'multiline';
// Attributes
el.getAttribute('href'); // Get attribute
el.setAttribute('href', '/new-url'); // Set attribute
el.removeAttribute('disabled'); // Remove attribute
el.hasAttribute('hidden'); // Check existence
// Properties (preferred over getAttribute for common attributes)
el.id; // Same as getAttribute('id')
el.className; // Same as getAttribute('class')
el.href; // Resolves to full URL (getAttribute returns raw)
el.checked; // Boolean for checkboxes/radio buttons
el.disabled; // Boolean
el.value; // For form elements
Creating & Inserting Elements
// Create elements
const div = document.createElement('div');
const h1 = document.createElement('h1');
const text = document.createTextNode('Hello World');
// Build structure
h1.textContent = 'My Title';
div.appendChild(h1);
div.appendChild(document.createElement('p')).textContent = 'Paragraph';
// Insert into page
document.body.appendChild(div);
// More insertion methods:
// insertBefore — insert before reference node
parent.insertBefore(newEl, existingEl);
// append() — can append multiple items + strings (modern!)
parent.append(child1, child2, 'text string');
// prepend() — insert at beginning of parent
parent.prepend(firstChild);
// before() / after() — insert relative to an element
existingEl.before(newEl); // Insert right BEFORE existingEl
existingEl.after(newEl); // Insert right AFTER existingEl
// replaceWith() — replace an element
oldEl.replaceWith(newEl);
// remove() — remove from DOM
el.remove(); // No need to call parent.removeChild(el)
// Practical example: Dynamic list
function renderList(items, containerId) {
const container = document.getElementById(containerId);
container.innerHTML = ''; // Clear existing
items.forEach(item => {
const li = document.createElement('li');
li.className = item.active ? 'active' : '';
li.innerHTML = `<strong>${item.name}</strong>: ${item.value}`;
const btn = document.createElement('button');
btn.textContent = 'Delete';
btn.onclick = () => li.remove();
li.appendChild(btn);
container.appendChild(li);
});
}
Styling Elements
// Individual styles
el.style.color = 'red';
el.style.backgroundColor = '#f0f0f0';
el.style.fontSize = '16px';
el.style.display = 'none'; // Hide
el.style.display = 'block'; // Show
// Better: CSS classes (separation of concerns)
el.classList.add('active');
el.classList.remove('hidden');
el.classList.toggle('visible'); // Toggle on/off
el.classList.contains('active'); // Check
// Multiple classes at once
el.classList.add('a', 'b', 'c');
el.classList.remove('x', 'y');
// Replace all classes
el.className = 'new-class another-class';
// Get computed style (final applied style)
const styles = window.getComputedStyle(el);
styles.color; // "rgb(255, 0, 0)"
styles.fontSize; // "16px"
styles.display; // "block"
Event Handling
// Basic event listener
button.addEventListener('click', function(event) {
console.log('Clicked!', event.target);
});
// Arrow function (no own `this`)
button.addEventListener('click', (e) => {
e.preventDefault(); // Prevent default behavior
e.stopPropagation(); // Stop bubbling to parent
console.log(e.target); // Element that triggered event
e.currentTarget; // Element listener is attached to
});
// Event delegation (one listener for many children!)
list.addEventListener('click', (e) => {
if (e.target.matches('li')) {
console.log('Clicked:', e.target.textContent);
}
});
// Benefits: Works for dynamically added elements too!
// Common events
element.addEventListener('input', handler); // Typing in input
element.addEventListener('change', handler); // Value changed + blur
element.addEventListener('submit', handler); // Form submitted
element.addEventListener('keydown', handler); // Key pressed
element.addEventListener('keyup', handler); // Key released
element.addEventListener('focus', handler); // Got focus
element.addEventListener('blur', handler); // Lost focus
window.addEventListener('scroll', handler); // Scrolling
window.addEventListener('resize', handler); // Window resized
document.addEventListener('DOMContentLoaded', fn); // HTML parsed, before images
// Remove listener (needs same function reference)
function handleClick() { /* ... */ }
button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick);
// Once option (auto-removes after firing)
button.addEventListener('click', handler, { once: true });
// Options object (useful options)
button.addEventListener('click', handler, {
once: false, // Remove after first trigger?
capture: false, // Capture phase vs bubble phase
passive: true, // Can't preventDefault (better scroll performance)
});
Traversing the DOM
// Parent/Child relationships
el.parentElement; // Direct parent
el.children; // Direct children (elements only)
el.childNodes; // All child nodes (including text nodes)
el.firstChild; // First child node
el.lastChild; // Last child node
el.firstElementChild; // First child ELEMENT
el.lastElementChild; // Last child ELEMENT
el.childElementCount; // Number of child elements
// Sibling relationships
el.previousSibling; // Previous sibling (any type)
el.nextSibling; // Next sibling (any type)
el.previousElementSibling; // Previous sibling element
el.nextElementSibling; // Next sibling element
// Closest ancestor matching selector (SUPER useful!)
el.closest('.container'); // Find parent with class "container"
el.closest('[data-type]'); // Find parent with data-type attribute
// Matches selector?
el.matches('.active'); // Does this element have class "active"?
el.contains(otherEl); // Is otherEl a descendant of this el?
Data Attributes (Custom Data)
<div data-user-id="123" data-role="admin" data-config='{"theme":"dark"}'></div>
// Read data attributes
el.dataset.userId; // "123"
el.dataset.role; // "admin"
el.dataset.config; // '{"theme":"dark"}'
JSON.parse(el.dataset.config); // { theme: 'dark' }
// Write data attributes
el.dataset.newAttr = 'value';
// Adds: data-new-attr="value" (camelCase → kebab-case)
// Check existence
'userId' in el.dataset; // true
// Use cases:
// - Store element metadata without polluting JS objects
// - Pass configuration to event handlers
// - Store state for UI components
// - Communication between components
Performance Tips
// ❌ Slow: Manipulating DOM in a loop
for (let i = 0; i < 1000; i++) {
document.body.appendChild(createDiv(i)); // 1000 reflows!
}
// ✅ Fast: DocumentFragment (batch DOM operations)
const fragment = new DocumentFragment();
for (let i = 0; i < 1000; i++) {
fragment.appendChild(createDiv(i));
}
document.body.appendChild(fragment); // Only 1 reflow!
// ✅ Fast: Clone template
const template = document.querySelector('#row-template');
data.forEach(item => {
const row = template.content.cloneNode(true);
row.querySelector('.name').textContent = item.name;
container.appendChild(row);
});
// ✅ Fast: innerHTML for bulk updates (vs individual createElement calls)
// For large lists, building one big string and setting innerHTML once is faster
// than creating hundreds of individual elements.
// Avoid layout thrashing (reading/writing alternately causes reflows)
// ❌ Bad (forces browser to recalculate layout each iteration):
for (const el of elements) {
console.log(el.offsetTop); // Read (forces reflow)
el.style.height = '100px'; // Write (triggers reflow)
}
// ✅ Good (batch reads, then batch writes):
const heights = Array.from(elements).map(el => el.offsetTop); // All reads
elements.forEach((el, i) => { el.style.height = heights[i] + 'px'; }); // All writes
Quick Reference Card
| Task | Method |
|---|---|
| Select by ID | getElementById() |
| Select by CSS |
querySelector() / querySelectorAll()
|
| Get/set text | textContent |
| Get/set HTML | innerHTML |
| Get/set value |
.value (forms) |
| Add/remove/toggle class | classList.add/remove/toggle() |
| Set inline style | el.style.property |
| Create element | createElement() |
| Append child |
appendChild() / append()
|
| Remove element | el.remove() |
| Insert before/after |
before() / after()
|
| Add event | addEventListener() |
| Find closest parent | closest() |
| Custom data | dataset.* |
| Batch operations | DocumentFragment |
What's your favorite DOM manipulation trick?
Follow @armorbreak for more JavaScript content.
Top comments (0)