DOM Manipulation
The Document Object Model (DOM) is a programming interface for HTML and XML documents. It represents the page as a tree of nodes that JavaScript can read and modify — enabling dynamic, interactive web pages.
The DOM Tree
Every HTML element becomes a node in the tree:
<html>
<body>
<h1 id="title">Hello</h1>
<p class="text">World</p>
</body>
</html>
documentis the entry pointdocument.documentElement→<html>html→body→h1,pare child nodes- Text inside elements are text nodes
Node Types
| Type | Example |
|---|---|
| Element node | <div>, <p> |
| Text node | "Hello" |
| Comment node | <!-- comment --> |
| Document node | document |
Selecting Elements
// By ID (returns one element or null)
const title = document.getElementById('title');
// By class (live HTMLCollection — updates when DOM changes)
const paragraphs = document.getElementsByClassName('text');
// By tag name (live HTMLCollection)
const allP = document.getElementsByTagName('p');
// Modern selectors (recommended — static NodeList)
const el = document.querySelector('#title');
const all = document.querySelectorAll('.text');
querySelector returns first match; querySelectorAll returns all matches. Prefer these for flexibility and consistency.
Reading and Modifying Content
const heading = document.querySelector('#title');
// Text content (safe — no HTML parsing)
heading.textContent = 'Hello, JavaScript!';
// Inner HTML (parses HTML — XSS risk with user input)
heading.innerHTML = '<em>Hello</em>, JavaScript!';
// Attributes
heading.setAttribute('class', 'highlight');
heading.getAttribute('id');
heading.removeAttribute('data-old');
// ClassList API
heading.classList.add('active');
heading.classList.remove('active');
heading.classList.toggle('active');
heading.classList.contains('active'); // boolean
heading.classList.replace('old', 'new');
Security: Always use textContent when inserting user-provided data. innerHTML executes embedded scripts if not sanitized.
Creating and Removing Elements
const newP = document.createElement('p');
newP.textContent = 'A new paragraph';
newP.classList.add('text');
document.body.appendChild(newP);
document.body.prepend(newP); // insert first child
newP.before(anotherElement); // sibling before
newP.after(anotherElement); // sibling after
const container = document.querySelector('#container');
container.insertBefore(newP, container.firstChild);
newP.remove(); // modern
// container.removeChild(newP); // older syntax
Document Fragments
Batch DOM updates for performance:
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
list.appendChild(fragment); // single reflow
Traversing the DOM
const node = document.querySelector('p');
node.parentElement;
node.parentNode; // includes document as parent of html
node.children; // element children only
node.childNodes; // all node types including text
node.nextElementSibling;
node.previousElementSibling;
node.firstElementChild;
node.lastElementChild;
node.closest('.container'); // nearest ancestor matching selector
DOMContentLoaded vs load
document.addEventListener('DOMContentLoaded', () => {
// HTML parsed, DOM ready — safe to query elements
});
window.addEventListener('load', () => {
// All resources loaded (images, stylesheets)
});
Place scripts at end of <body> or use defer on <script defer src="app.js">.
Complete Example — Todo List
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DOM Example</title>
<style>
.done { text-decoration: line-through; color: gray; }
</style>
</head>
<body>
<h1 id="title">Todo List</h1>
<ul id="list"></ul>
<input id="input" type="text" placeholder="New task">
<button id="addBtn">Add</button>
<script>
const list = document.querySelector('#list');
const input = document.querySelector('#input');
const addBtn = document.querySelector('#addBtn');
function addTask(text) {
const li = document.createElement('li');
li.textContent = text;
li.addEventListener('click', () => li.classList.toggle('done'));
list.appendChild(li);
}
addBtn.addEventListener('click', () => {
const text = input.value.trim();
if (text) {
addTask(text);
input.value = '';
input.focus();
}
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') addBtn.click();
});
</script>
</body>
</html>
Best Practices
- Cache DOM references — don’t query inside tight loops
- Batch updates with DocumentFragment or
display: noneduring bulk changes - Use
textContentfor user data — prevent XSS - Prefer
classListover string manipulation ofclassName - Use event delegation for dynamic lists (see Events chapter)
Common Mistakes
| Mistake | Fix |
|---|---|
| Script runs before DOM exists | Use DOMContentLoaded or defer |
innerHTML with user input |
Use textContent or sanitize |
| Live collection surprise | getElementsByClassName updates live — copy to array if iterating while modifying |
Forgetting preventDefault on forms |
See Events chapter |
Troubleshooting
querySelector returns null
- Element doesn’t exist yet — wait for
DOMContentLoaded - Typo in selector — verify in DevTools Console:
document.querySelector('#myId')
Changes not visible
- Modified wrong element — log
elementto confirm reference - CSS hiding element — check computed styles
Performance slow with many nodes
- Use DocumentFragment; virtual DOM libraries (React) for complex UIs
The DOM is JavaScript’s bridge to the page — master selection, mutation, and traversal for interactive applications.