~/.claude/skills/ or any Agent-Skills-compatible runtime for AI-assisted DOM work.
DOM
- Tree structure representing HTML document
- Browser API for manipulating page structure
- Live representation updated by JavaScript
Key Insight
The DOM is the “living blueprint” of your webpage—it’s not the HTML file you wrote, but the browser’s in-memory tree structure built from that HTML, and it changes in real-time as JavaScript manipulates it. When you document.querySelector('.button') and change its text, you’re modifying the DOM (which updates the screen), not the original HTML file. Understanding this distinction is crucial: HTML is the source code, the DOM is the running program. That’s why “View Source” shows your original HTML, but “Inspect Element” shows the current DOM (which might be completely different after React/Vue/Angular has manipulated it).
Detailed Description
Document Object Model (DOM) is a set of APIs for controlling HTML and styling information that makes heavy use of the Document object. Each browser tab holds its own DOM — a tree structure built from the page’s HTML that programming languages can read, traverse, and mutate.
The browser parses HTML into a tree of objects called nodes. Every HTML tag becomes an element node, text becomes a text node, and the document itself is the root node. This tree structure mirrors the nesting of HTML tags, creating parent-child relationships that JavaScript can traverse and manipulate.
Key aspects of the DOM:
- Tree structure - Hierarchical representation of document elements
- Live updates - Changes to DOM immediately reflect on screen
- Language-agnostic - Defined as API, accessible from JavaScript, Python, etc.
- Event-driven - Supports event listeners for user interaction
- Browser-managed - Browser keeps DOM and screen in sync
Code Examples
Basic Example: DOM Traversal and Manipulation
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>DOM Manipulation Example</title>
</head>
<body>
<section id="main">
<h1>Welcome</h1>
<p class="intro">This is an introduction.</p>
<ul id="list">
<li>Item 1</li>
<li>Item 2</li>
</ul>
</section>
<script>
// Selecting elements
const heading = document.querySelector('h1');
const intro = document.querySelector('.intro');
const list = document.getElementById('list');
// Reading content
console.log(heading.textContent); // "Welcome"
console.log(intro.innerHTML); // "This is an introduction."
// Modifying content
heading.textContent = 'Hello, World!';
intro.innerHTML = '<strong>Updated</strong> introduction.';
// Creating and appending elements
const newItem = document.createElement('li');
newItem.textContent = 'Item 3';
list.appendChild(newItem);
// Modifying styles
heading.style.color = 'blue';
heading.style.fontSize = '2rem';
// Modifying attributes
intro.setAttribute('data-status', 'updated');
// Removing elements
const firstItem = list.querySelector('li');
firstItem.remove();
</script>
</body>
</html>
Practical Example: Dynamic Content and Event Handling
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Todo List - DOM Example</title>
<style>
.completed { text-decoration: line-through; color: gray; }
.todo-item { cursor: pointer; margin: 0.5rem 0; }
</style>
</head>
<body>
<div id="app">
<h1>Todo List</h1>
<input type="text" id="todoInput" placeholder="Add new todo...">
<button id="addBtn">Add</button>
<ul id="todoList"></ul>
</div>
<script>
const todoInput = document.getElementById('todoInput');
const addBtn = document.getElementById('addBtn');
const todoList = document.getElementById('todoList');
// Add todo function
function addTodo() {
const text = todoInput.value.trim();
if (!text) return;
// Create elements
const li = document.createElement('li');
li.className = 'todo-item';
const span = document.createElement('span');
span.textContent = text;
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.style.marginLeft = '1rem';
// Assemble DOM structure
li.appendChild(span);
li.appendChild(deleteBtn);
todoList.appendChild(li);
// Clear input
todoInput.value = '';
// Add event listeners
span.addEventListener('click', () => {
li.classList.toggle('completed');
});
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent span click
li.remove();
});
}
// Event listeners
addBtn.addEventListener('click', addTodo);
todoInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') addTodo();
});
</script>
</body>
</html>
Advanced Example: DOM Performance and Fragment
// Performance optimization with DocumentFragment
function renderLargeList(items) {
const list = document.getElementById('large-list');
// ❌ BAD: Causes reflow for each append
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item.name;
list.appendChild(li); // Reflow!
});
// ✅ GOOD: Single reflow with DocumentFragment
const fragment = document.createDocumentFragment();
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item.name;
fragment.appendChild(li); // In memory, no reflow
});
list.appendChild(fragment); // Single reflow
}
// DOM traversal patterns
function findElements() {
const parent = document.querySelector('.container');
// Children vs childNodes
console.log(parent.children); // HTMLCollection (element nodes only)
console.log(parent.childNodes); // NodeList (all nodes including text)
// Siblings
const first = parent.firstElementChild;
const next = first.nextElementSibling;
const prev = next.previousElementSibling;
// Parent traversal
const grandparent = first.parentElement.parentElement;
// Closest (search up the tree)
const card = first.closest('.card');
// Query within element
const buttons = parent.querySelectorAll('button');
}
// Observing DOM changes
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
console.log('Child nodes changed:', mutation.addedNodes, mutation.removedNodes);
} else if (mutation.type === 'attributes') {
console.log('Attribute changed:', mutation.attributeName);
}
});
});
const targetNode = document.getElementById('observed');
observer.observe(targetNode, {
childList: true,
attributes: true,
subtree: true // Observe descendants too
});
// Virtual scrolling for large lists
class VirtualList {
constructor(container, items, rowHeight) {
this.container = container;
this.items = items;
this.rowHeight = rowHeight;
this.visibleStart = 0;
this.visibleEnd = 0;
this.render();
this.container.addEventListener('scroll', () => this.render());
}
render() {
const scrollTop = this.container.scrollTop;
const containerHeight = this.container.clientHeight;
// Calculate visible range
this.visibleStart = Math.floor(scrollTop / this.rowHeight);
this.visibleEnd = Math.ceil((scrollTop + containerHeight) / this.rowHeight);
// Clear and render only visible items
this.container.innerHTML = '';
const fragment = document.createDocumentFragment();
for (let i = this.visibleStart; i < this.visibleEnd; i++) {
if (this.items[i]) {
const div = document.createElement('div');
div.style.height = `${this.rowHeight}px`;
div.textContent = this.items[i];
fragment.appendChild(div);
}
}
this.container.appendChild(fragment);
}
}
Common Mistakes
1. Modifying DOM Inside Loops Without Batching
Mistake: Causing multiple reflows by manipulating DOM in loop.
// ❌ BAD: Reflow on every iteration
const list = document.getElementById('list');
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item.name;
list.appendChild(li); // Reflow! (100 items = 100 reflows)
});
// Causes layout thrashing, very slow
// ✅ GOOD: Batch with DocumentFragment
const list = document.getElementById('list');
const fragment = document.createDocumentFragment();
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item.name;
fragment.appendChild(li); // In memory
});
list.appendChild(fragment); // Single reflow
Why it matters: Each DOM append can trigger a reflow (recalculating layout). Batching with a fragment collapses N reflows into one, which can be the difference between a janky list and an instant one.
2. Using innerHTML for User-Generated Content
Mistake: Security vulnerability (XSS) when using innerHTML with untrusted data.
// ❌ BAD: XSS vulnerability
const userInput = getUserInput(); // "<img src=x onerror=alert('XSS')>"
const div = document.getElementById('content');
div.innerHTML = userInput; // Executes malicious script!
// ✅ GOOD: Use textContent or createElement
const userInput = getUserInput();
const div = document.getElementById('content');
div.textContent = userInput; // Safe - renders as text, not HTML
// Or for complex content
const p = document.createElement('p');
p.textContent = userInput;
div.appendChild(p);
Why it matters: innerHTML parses its value as HTML, so any <script>, onerror=, or other handler in untrusted input becomes live code (the classic XSS vector). textContent treats the value as plain text — no parsing, no execution.
3. Not Removing Event Listeners
Mistake: Memory leaks from orphaned event listeners.
// ❌ BAD: Event listener not removed
function attachHandler() {
const button = document.getElementById('btn');
button.addEventListener('click', () => {
console.log('Clicked');
});
button.remove(); // Button removed but listener still in memory!
}
// Repeated calls leak memory
// ✅ GOOD: Remove listener before removing element
function attachHandler() {
const button = document.getElementById('btn');
function handleClick() {
console.log('Clicked');
}
button.addEventListener('click', handleClick);
// Later, when removing
button.removeEventListener('click', handleClick);
button.remove();
}
// Or use AbortController (modern approach)
const controller = new AbortController();
button.addEventListener('click', handleClick, {
signal: controller.signal
});
// Clean up
controller.abort(); // Removes all listeners with this signal
button.remove();
Why it matters: Orphaned listeners prevent garbage collection, causing memory leaks in SPAs.