DOM Manipulation
The DOM (Document Object Model) is an interface that represents an HTML document as a tree structure. By manipulating the DOM with JavaScript, you can dynamically change the page structure, styles, and content.
DOM Tree Structure
An HTML document is represented as a hierarchical tree of nodes.
document
└── html
├── head
│ ├── title (text: "My Page")
│ └── meta
└── body
├── h1 (text: "Title")
├── div.container
│ ├── p (text: "Paragraph 1")
│ └── ul
│ ├── li (text: "Item 1")
│ └── li (text: "Item 2")
└── footer
// Node types
// Node.ELEMENT_NODE (1) - <div>, <p>, etc.
// Node.TEXT_NODE (3) - text content
// Node.COMMENT_NODE (8) - <!-- comment -->
const h1 = document.querySelector('h1');
console.log(h1.nodeType); // 1 (ELEMENT_NODE)
console.log(h1.nodeName); // 'H1'
console.log(h1.nodeValue); // null (element nodes return null)
const textNode = h1.firstChild;
console.log(textNode.nodeType); // 3 (TEXT_NODE)
console.log(textNode.nodeValue); // 'Title'
querySelector / querySelectorAll
Search for elements using CSS selectors.
// querySelector: returns the first matching element
const title = document.querySelector('h1');
const btn = document.querySelector('.btn-primary');
const input = document.querySelector('input[type="email"]');
const firstItem = document.querySelector('ul > li:first-child');
// querySelectorAll: returns a NodeList of all matching elements
const allButtons = document.querySelectorAll('button');
const allInputs = document.querySelectorAll('input, select, textarea');
// NodeList is not an Array, so conversion is needed to use Array methods
const buttonsArray = Array.from(allButtons);
const buttonsSpread = [...allButtons];
// forEach can be used directly on NodeList
allButtons.forEach(btn => {
btn.addEventListener('click', handleClick);
});
// Search within a specific element (specifying context)
const container = document.querySelector('.container');
const innerItems = container.querySelectorAll('li'); // only li inside .container
// Various selectors
document.querySelector('[data-id="123"]'); // data attribute
document.querySelector(':not(.disabled)'); // negation selector
document.querySelector('.parent > .direct-child'); // direct child
Other Traversal Methods
// Search by ID (fastest)
const app = document.getElementById('app');
// Search by class (returns HTMLCollection - a live collection)
const items = document.getElementsByClassName('item');
// Search by tag name
const divs = document.getElementsByTagName('div');
// Family relationship traversal
const parent = element.parentElement;
const children = element.children; // HTMLCollection (element nodes only)
const childNodes = element.childNodes; // NodeList (includes text nodes)
const firstChild = element.firstElementChild;
const lastChild = element.lastElementChild;
const nextSibling = element.nextElementSibling;
const prevSibling = element.previousElementSibling;
// closest: find the nearest ancestor element
const listItem = document.querySelector('li');
const parentList = listItem.closest('ul'); // nearest ul
const section = listItem.closest('section'); // nearest section
Creating and Inserting Elements
createElement and textContent
// Create an element
const div = document.createElement('div');
div.className = 'card';
div.id = 'card-1';
div.textContent = 'Card content'; // XSS safe
// Create a text node
const textNode = document.createTextNode('Text content');
// appendChild: insert as the last child
const container = document.querySelector('.container');
container.appendChild(div);
// insertBefore: insert before a specific element
const referenceNode = container.querySelector('.existing');
container.insertBefore(div, referenceNode);
// Set attributes
div.setAttribute('data-id', '123');
div.setAttribute('aria-label', 'Card description');
div.getAttribute('data-id'); // '123'
div.removeAttribute('aria-label');
div.hasAttribute('data-id'); // true
// Set multiple attributes at once
Object.assign(div, {
id: 'card-1',
className: 'card active',
title: 'Card title',
tabIndex: 0
});
insertAdjacentHTML
Inserts an HTML string at a specific position. More efficient than innerHTML.
const container = document.querySelector('.container');
// Position options:
// 'beforebegin' - before the element (same level)
// 'afterbegin' - as the first child of the element
// 'beforeend' - as the last child of the element
// 'afterend' - after the element (same level)
container.insertAdjacentHTML('beforeend', `
<div class="card">
<h3>New Card</h3>
<p>Card content.</p>
</div>
`);
container.insertAdjacentHTML('afterbegin', '<div class="banner">Notice</div>');
container.insertAdjacentHTML('beforebegin', '<div class="header-area">Header</div>');
// insertAdjacentElement: insert an element
const newItem = document.createElement('li');
newItem.textContent = 'New item';
existingItem.insertAdjacentElement('afterend', newItem);
// insertAdjacentText: insert text (XSS safe)
element.insertAdjacentText('beforeend', 'Additional text');
append / prepend / replaceWith / remove
ES2015+ methods enable more intuitive manipulation.
const list = document.querySelector('ul');
// append: add to the end (multiple items, text allowed)
const li1 = document.createElement('li');
const li2 = document.createElement('li');
list.append(li1, li2, 'text is also allowed');
// prepend: add to the beginning
list.prepend(document.createElement('li'));
// replaceWith: replace an element
const oldItem = document.querySelector('.old');
const newItem = document.createElement('div');
oldItem.replaceWith(newItem);
// remove: remove an element
const target = document.querySelector('.remove-me');
target.remove();
// cloneNode: copy an element
const original = document.querySelector('.template');
const clone = original.cloneNode(true); // true: deep copy including children
clone.id = 'clone-1';
document.body.appendChild(clone);
Performance Optimization with DocumentFragment
DOM manipulation triggers reflow and repaint. Using DocumentFragment allows batch processing of operations.
// Bad approach: DOM manipulation every iteration → 1000 reflows
const list = document.querySelector('#large-list');
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
list.appendChild(li); // DOM update triggered every iteration!
}
// Good approach: batch processing with DocumentFragment → 1 reflow
const list = document.querySelector('#large-list');
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li); // manipulate only in memory
}
list.appendChild(fragment); // insert into DOM only once
// Real-world: render a data array as a list
function renderUserList(users) {
const list = document.querySelector('#user-list');
const fragment = document.createDocumentFragment();
users.forEach(user => {
const li = document.createElement('li');
li.className = 'user-item';
li.innerHTML = `
<img src="${user.avatar}" alt="${user.name}">
<span class="name">${user.name}</span>
<span class="email">${user.email}</span>
`;
li.dataset.userId = user.id;
fragment.appendChild(li);
});
list.replaceChildren(fragment); // replace all existing content and insert
}
// Clear content with replaceChildren
list.replaceChildren(); // no arguments removes all children
Understanding the Virtual DOM Concept (React Comparison)
Real DOM manipulation is costly. Frameworks like React use a Virtual DOM.
// Why real DOM manipulation is expensive
// 1. Layout calculation (Reflow)
// 2. Screen painting (Repaint)
// 3. Layer compositing (Composite)
// Understanding the virtual DOM concept in pure JavaScript
// Virtual DOM is a JavaScript representation of the real DOM
const virtualDOM = {
type: 'div',
props: { className: 'container' },
children: [
{
type: 'h1',
props: {},
children: ['Title']
},
{
type: 'p',
props: { className: 'text' },
children: ['Content']
}
]
};
// Virtual DOM → real DOM conversion
function createElement(vnode) {
if (typeof vnode === 'string') {
return document.createTextNode(vnode);
}
const el = document.createElement(vnode.type);
// Apply props
Object.entries(vnode.props || {}).forEach(([key, value]) => {
if (key === 'className') {
el.className = value;
} else {
el.setAttribute(key, value);
}
});
// Recursively create child nodes
(vnode.children || []).forEach(child => {
el.appendChild(createElement(child));
});
return el;
}
// React's diff algorithm concept
// Compare the previous Virtual DOM with the new Virtual DOM
// and reflect only the changed parts in the real DOM (minimal DOM manipulation)
Using dataset
Store custom data in HTML elements via data-* attributes.
// HTML: <div id="user" data-user-id="123" data-user-name="Alice" data-is-admin="true">
const userEl = document.getElementById('user');
// Access via dataset (automatic camelCase conversion)
userEl.dataset.userId; // '123' (always a string!)
userEl.dataset.userName; // 'Alice'
userEl.dataset.isAdmin; // 'true' (string!)
// Set a value
userEl.dataset.lastLogin = new Date().toISOString();
// HTML: data-last-login="2024-03-15T..."
// Delete a value
delete userEl.dataset.isAdmin;
// Type conversion is needed (always strings)
const userId = Number(userEl.dataset.userId); // 123
const isAdmin = userEl.dataset.isAdmin === 'true'; // boolean
// Real-world: using in event delegation
document.querySelector('#product-list').addEventListener('click', (e) => {
const button = e.target.closest('[data-action]');
if (!button) return;
const { action, productId } = button.dataset;
switch (action) {
case 'add-to-cart':
addToCart(productId);
break;
case 'wishlist':
addToWishlist(productId);
break;
case 'delete':
deleteProduct(productId);
break;
}
});
// Storing JSON
const config = { theme: 'dark', lang: 'en', version: 2 };
element.dataset.config = JSON.stringify(config);
const restored = JSON.parse(element.dataset.config);
Using classList
const el = document.querySelector('.card');
// Basic operations
el.classList.add('active'); // add class
el.classList.remove('hidden'); // remove class
el.classList.toggle('selected'); // remove if present, add if not
el.classList.contains('active'); // check presence (boolean)
el.classList.replace('old', 'new'); // replace
// Manipulate multiple classes at once
el.classList.add('visible', 'loaded', 'ready');
el.classList.remove('loading', 'placeholder');
// toggle with second argument: force add/remove
el.classList.toggle('active', isActive); // add if isActive is true, remove if false
// Real-world: theme toggle
const themeToggle = document.querySelector('#theme-toggle');
themeToggle.addEventListener('click', () => {
document.documentElement.classList.toggle('dark-theme');
localStorage.setItem('theme', document.documentElement.classList.contains('dark-theme') ? 'dark' : 'light');
});
// Real-world: show/hide modal
function showModal(modalId) {
const modal = document.getElementById(modalId);
modal.classList.add('visible');
document.body.classList.add('modal-open');
modal.setAttribute('aria-hidden', 'false');
}
function hideModal(modalId) {
const modal = document.getElementById(modalId);
modal.classList.remove('visible');
document.body.classList.remove('modal-open');
modal.setAttribute('aria-hidden', 'true');
}
// Managing classes on multiple elements
function setActiveTab(tabId) {
// Deactivate all tabs
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
tab.setAttribute('aria-selected', 'false');
});
// Activate the selected tab
const activeTab = document.querySelector(`[data-tab="${tabId}"]`);
activeTab?.classList.add('active');
activeTab?.setAttribute('aria-selected', 'true');
}
Real-World Example: Dynamic Table Rendering
function renderTable(data, columns) {
const fragment = document.createDocumentFragment();
const table = document.createElement('table');
table.className = 'data-table';
// Create header
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
columns.forEach(col => {
const th = document.createElement('th');
th.textContent = col.label;
th.dataset.field = col.field;
th.classList.add('sortable');
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// Create body
const tbody = document.createElement('tbody');
data.forEach(row => {
const tr = document.createElement('tr');
tr.dataset.id = row.id;
columns.forEach(col => {
const td = document.createElement('td');
td.textContent = col.format ? col.format(row[col.field]) : row[col.field];
tr.appendChild(td);
});
tbody.appendChild(tr);
});
table.appendChild(tbody);
fragment.appendChild(table);
return fragment;
}
// Usage
const users = [
{ id: 1, name: 'Alice', age: 30, email: 'alice@example.com' },
{ id: 2, name: 'Bob', age: 25, email: 'bob@example.com' },
];
const columns = [
{ field: 'name', label: 'Name' },
{ field: 'age', label: 'Age', format: v => `${v} yrs` },
{ field: 'email', label: 'Email' },
];
document.querySelector('#table-container').appendChild(renderTable(users, columns));
Expert Tips
1. Use textContent instead of innerHTML
// Dangerous: XSS attack possible
const userInput = '<script>alert("xss")</script>';
element.innerHTML = userInput; // script executes!
// Safe: textContent does not parse as HTML
element.textContent = userInput; // displayed as plain text
2. Preventing Layout Thrashing
// Bad: alternating reads and writes causes reflow every iteration
elements.forEach(el => {
const height = el.offsetHeight; // read (reflow)
el.style.height = `${height * 2}px`; // write (invalidates layout)
// On the next iteration, reading offsetHeight triggers reflow again!
});
// Good: separate reads and writes
const heights = elements.map(el => el.offsetHeight); // all reads
elements.forEach((el, i) => {
el.style.height = `${heights[i] * 2}px`; // all writes
});
3. Using IntersectionObserver and MutationObserver (see section 8.5)
// Add a class when an element enters the viewport (scroll animation)
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-in');
}
});
}, { threshold: 0.1 });
document.querySelectorAll('.animated').forEach(el => observer.observe(el));
4. Pattern for checking element existence
// Safe access with optional chaining
document.querySelector('#optional-element')?.classList.add('active');
document.querySelector('#form')?.addEventListener('submit', handleSubmit);
// Null check when handling multiple elements
const modal = document.querySelector('#modal');
if (modal) {
modal.classList.add('visible');
}