The Document Object Model (DOM) is a programming interface for HTML and XML documents. It represents the page structure as a tree of objects that can be manipulated with JavaScript, enabling dynamic web pages and interactive user interfaces.
In this tutorial, we'll explore the DOM structure, how to access and manipulate DOM elements, handle events, and create dynamic web applications using JavaScript's DOM APIs.
Understanding the DOM
The DOM is a hierarchical tree structure that represents all elements on a web page.
DOM Tree Structure
<!DOCTYPE html>
<html>
<head>
<title>My Page</title>
</head>
<body>
<div id="container">
<h1>Hello World</h1>
<p class="text">Welcome to my website</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>
</body>
</html>
DOM Node Types
// DOM node types
const nodeTypes = {
1: 'ELEMENT_NODE', // <div>, <p>, etc.
2: 'ATTRIBUTE_NODE', // class, id, etc.
3: 'TEXT_NODE', // Text content
4: 'CDATA_SECTION_NODE',
5: 'ENTITY_REFERENCE_NODE',
6: 'ENTITY_NODE',
7: 'PROCESSING_INSTRUCTION_NODE',
8: 'COMMENT_NODE', // <!-- comment -->
9: 'DOCUMENT_NODE', // The document itself
10: 'DOCUMENT_TYPE_NODE',
11: 'DOCUMENT_FRAGMENT_NODE',
12: 'NOTATION_NODE'
};
// Check node type
const element = document.getElementById('container');
console.log(element.nodeType); // 1 (ELEMENT_NODE)
console.log(element.nodeName); // "DIV"
DOM Relationships
// DOM node relationships
const container = document.getElementById('container');
// Parent node
console.log(container.parentNode); // <body>
console.log(container.parentElement); // <body> (same as parentNode for elements)
// Child nodes
console.log(container.childNodes); // NodeList with all nodes (including text)
console.log(container.children); // HTMLCollection with only element nodes
// First and last child
console.log(container.firstChild); // First node (might be text)
console.log(container.firstElementChild); // First element child
console.log(container.lastChild); // Last node
console.log(container.lastElementChild); // Last element child
// Siblings
console.log(container.previousSibling); // Previous node
console.log(container.previousElementSibling); // Previous element
console.log(container.nextSibling); // Next node
console.log(container.nextElementSibling); // Next element
// Children iteration
for (let i = 0; i < container.children.length; i++) {
console.log(container.children[i]);
}
Accessing DOM Elements
Various methods to select and access DOM elements using JavaScript.
getElementById
// Get element by ID (fastest method)
const header = document.getElementById('header');
const container = document.getElementById('main-container');
// Returns null if not found
const nonExistent = document.getElementById('non-existent');
console.log(nonExistent); // null
// Safe usage
function getElementById(id) {
const element = document.getElementById(id);
if (!element) {
console.warn(`Element with id "${id}" not found`);
return null;
}
return element;
}
const safeElement = getElementById('header');
querySelector Methods
// querySelector - returns first matching element
const firstButton = document.querySelector('button');
const firstClass = document.querySelector('.highlight');
const firstId = document.querySelector('#header');
const firstAttribute = document.querySelector('[data-role="button"]');
// Complex selectors
const nested = document.querySelector('#container .item');
const multiple = document.querySelector('div.highlight, span.highlight');
const pseudo = document.querySelector('li:first-child');
// querySelectorAll - returns all matching elements
const allButtons = document.querySelectorAll('button');
const allClasses = document.querySelectorAll('.highlight');
const allAttributes = document.querySelectorAll('[data-role]');
// NodeList vs Array
const nodeList = document.querySelectorAll('div');
console.log(nodeList.forEach); // undefined (NodeList)
// Convert NodeList to Array
const array = Array.from(nodeList);
const array2 = [...nodeList]; // Spread operator
// NodeList iteration (modern browsers)
nodeList.forEach(node => console.log(node));
getElementsByClassName
// Get elements by class name
const highlights = document.getElementsByClassName('highlight');
const containers = document.getElementsByClassName('container');
// HTMLCollection (live collection)
const liveCollection = document.getElementsByClassName('item');
console.log(liveCollection.length); // Current count
// Add new element with same class
document.body.innerHTML += '<div class="item">New item</div>';
console.log(liveCollection.length); // Updated count
// Convert to Array for static collection
const staticCollection = Array.from(document.getElementsByClassName('item'));
// Context-specific selection
const container = document.getElementById('container');
const containerItems = container.getElementsByClassName('item');
getElementsByTagName
// Get elements by tag name
const allDivs = document.getElementsByTagName('div');
const allParagraphs = document.getElementsByTagName('p');
const allLinks = document.getElementsByTagName('a');
// Context-specific
const container = document.getElementById('container');
const containerDivs = container.getElementsByTagName('div');
// Special collections
console.log(document.forms); // All forms
console.log(document.images); // All images
console.log(document.links); // All links
console.log(document.scripts); // All scripts
Manipulating DOM Elements
Methods to modify, create, and remove DOM elements.
Content Manipulation
// Get and set text content
const paragraph = document.querySelector('p');
console.log(paragraph.textContent); // Get text content
paragraph.textContent = 'New text content'; // Set text content
// innerHTML - use with caution (XSS risk)
const container = document.getElementById('container');
container.innerHTML = '<h2>New Content</h2><p>Paragraph</p>';
// Safe HTML insertion
function setHTML(element, html) {
// Sanitize HTML in production
element.innerHTML = html;
}
// textContent vs innerHTML
const div = document.createElement('div');
div.textContent = '<strong>Bold</strong>';
console.log(div.innerHTML); // "<strong>Bold</strong>"
div.innerHTML = '<strong>Bold</strong>';
console.log(div.textContent); // "Bold"
Attribute Manipulation
// Get and set attributes
const image = document.querySelector('img');
console.log(image.getAttribute('src')); // Get attribute
image.setAttribute('src', 'new-image.jpg'); // Set attribute
image.setAttribute('alt', 'Description'); // Set attribute
// Check if attribute exists
if (image.hasAttribute('alt')) {
console.log('Alt attribute exists');
}
// Remove attribute
image.removeAttribute('title');
// Direct property access (for common attributes)
console.log(image.id); // Get ID
image.id = 'new-id'; // Set ID
image.className = 'new-class'; // Set class
// Data attributes
image.dataset.userId = '123';
console.log(image.dataset.userId); // '123'
image.dataset.role = 'button';
console.log(image.getAttribute('data-role')); // 'button'
Style Manipulation
// Inline styles
const element = document.getElementById('myElement');
element.style.color = 'red';
element.style.backgroundColor = 'blue';
element.style.fontSize = '16px';
// CSS custom properties (variables)
element.style.setProperty('--primary-color', '#007bff');
element.style.getPropertyValue('--primary-color');
// Computed styles
const computed = getComputedStyle(element);
console.log(computed.color); // Computed color
console.log(computed.backgroundColor); // Computed background
// Class manipulation
element.classList.add('active');
element.classList.remove('inactive');
element.classList.toggle('highlight');
element.classList.contains('active'); // true
// Multiple classes
element.classList.add('class1', 'class2', 'class3');
element.classList.remove('class1', 'class2');
// Replace class
element.classList.replace('old-class', 'new-class');
Creating and Removing Elements
Dynamic creation and removal of DOM elements.
Creating Elements
// Create element
const newDiv = document.createElement('div');
const newParagraph = document.createElement('p');
const newButton = document.createElement('button');
// Set attributes and content
newDiv.id = 'new-div';
newDiv.className = 'container';
newDiv.textContent = 'New div content';
newParagraph.textContent = 'New paragraph';
newButton.textContent = 'Click me';
newButton.setAttribute('type', 'button');
// Create text node
const textNode = document.createTextNode('Text content');
newParagraph.appendChild(textNode);
// Create comment
const comment = document.createComment('This is a comment');
document.body.appendChild(comment);
// Create document fragment
const fragment = document.createDocumentFragment();
fragment.appendChild(newDiv);
fragment.appendChild(newParagraph);
fragment.appendChild(newButton);
// Add fragment to DOM (single reflow)
document.body.appendChild(fragment);
Appending and Inserting
// Append child
const parent = document.getElementById('parent');
const child = document.createElement('div');
parent.appendChild(child);
// Insert before
const reference = document.getElementById('reference');
const newElement = document.createElement('span');
parent.insertBefore(newElement, reference);
// Append multiple children
const elements = [
document.createElement('div'),
document.createElement('p'),
document.createElement('span')
];
elements.forEach(el => parent.appendChild(el));
// prepend() - insert at beginning (modern browsers)
parent.prepend(newElement);
// append() - append multiple elements (modern browsers)
parent.append(newElement, anotherElement);
// after() and before() (modern browsers)
reference.after(newElement);
reference.before(newElement);
Removing Elements
// Remove child
const parent = document.getElementById('parent');
const child = document.getElementById('child');
parent.removeChild(child);
// Remove element itself (modern browsers)
element.remove();
// Remove all children
const container = document.getElementById('container');
while (container.firstChild) {
container.removeChild(container.firstChild);
}
// Modern way to remove all children
container.innerHTML = ''; // Fast but not memory efficient
container.textContent = ''; // Removes all content
// Replace child
const oldChild = document.getElementById('old');
const newChild = document.createElement('div');
parent.replaceChild(newChild, oldChild);
// replaceWith() (modern browsers)
oldElement.replaceWith(newElement);
DOM Events
Handling user interactions and DOM events.
Event Listeners
// Add event listener
const button = document.getElementById('myButton');
button.addEventListener('click', function(event) {
console.log('Button clicked!');
console.log('Event object:', event);
});
// Arrow function
button.addEventListener('click', (event) => {
console.log('Arrow function handler');
});
// Function reference
function handleClick(event) {
console.log('Function reference handler');
}
button.addEventListener('click', handleClick);
// Multiple listeners
button.addEventListener('click', () => console.log('First listener'));
button.addEventListener('click', () => console.log('Second listener'));
// Remove event listener
button.removeEventListener('click', handleClick);
// Event options
button.addEventListener('click', handler, {
once: true, // Remove after first call
capture: false, // Use capture phase
passive: true // Improve performance
});
Common Events
// Mouse events
element.addEventListener('click', handleEvent);
element.addEventListener('dblclick', handleEvent);
element.addEventListener('mousedown', handleEvent);
element.addEventListener('mouseup', handleEvent);
element.addEventListener('mousemove', handleEvent);
element.addEventListener('mouseover', handleEvent);
element.addEventListener('mouseout', handleEvent);
element.addEventListener('mouseenter', handleEvent);
element.addEventListener('mouseleave', handleEvent);
// Keyboard events
element.addEventListener('keydown', handleEvent);
element.addEventListener('keyup', handleEvent);
element.addEventListener('keypress', handleEvent);
// Form events
form.addEventListener('submit', handleEvent);
input.addEventListener('input', handleEvent);
input.addEventListener('change', handleEvent);
input.addEventListener('focus', handleEvent);
input.addEventListener('blur', handleEvent);
// Window/document events
window.addEventListener('load', handleEvent);
window.addEventListener('resize', handleEvent);
window.addEventListener('scroll', handleEvent);
document.addEventListener('DOMContentLoaded', handleEvent);
Event Object
// Event object properties
function handleEvent(event) {
console.log('Event type:', event.type);
console.log('Target:', event.target);
console.log('Current target:', event.currentTarget);
console.log('Timestamp:', event.timeStamp);
// Mouse events
console.log('Client X:', event.clientX);
console.log('Client Y:', event.clientY);
console.log('Button:', event.button);
console.log('Which:', event.which);
// Keyboard events
console.log('Key:', event.key);
console.log('Code:', event.code);
console.log('AltKey:', event.altKey);
console.log('CtrlKey:', event.ctrlKey);
console.log('ShiftKey:', event.shiftKey);
// Prevent default behavior
event.preventDefault();
// Stop propagation
event.stopPropagation();
// Stop immediate propagation
event.stopImmediatePropagation();
}
// Form submission example
document.querySelector('form').addEventListener('submit', function(event) {
event.preventDefault(); // Prevent page reload
const formData = new FormData(this);
const data = Object.fromEntries(formData);
console.log('Form data:', data);
});
Event Delegation
Efficient event handling using event delegation.
Basic Delegation
<!-- HTML Structure -->
<ul id="item-list">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
// Event delegation - single listener for multiple elements
const list = document.getElementById('item-list');
list.addEventListener('click', function(event) {
// Check if clicked element is an LI
if (event.target.tagName === 'LI') {
console.log('Clicked item:', event.target.textContent);
// Add highlight class
event.target.classList.add('highlight');
// Remove highlight from other items
Array.from(list.children).forEach(item => {
if (item !== event.target) {
item.classList.remove('highlight');
}
});
}
});
// More specific delegation
list.addEventListener('click', function(event) {
const item = event.target.closest('li');
if (item) {
console.log('Clicked LI:', item.textContent);
}
});
// Delegation with data attributes
document.addEventListener('click', function(event) {
if (event.target.dataset.action) {
const action = event.target.dataset.action;
console.log('Action:', action);
switch(action) {
case 'delete':
deleteItem(event.target);
break;
case 'edit':
editItem(event.target);
break;
}
}
});
Advanced Delegation
// Dynamic content delegation
const container = document.getElementById('dynamic-container');
container.addEventListener('click', function(event) {
const button = event.target.closest('[data-action]');
if (!button) return;
const action = button.dataset.action;
const targetId = button.dataset.target;
switch(action) {
case 'remove':
const target = document.getElementById(targetId);
if (target) target.remove();
break;
case 'toggle':
const element = document.getElementById(targetId);
if (element) element.classList.toggle('hidden');
break;
}
});
// Generic delegation utility
function delegate(parent, selector, event, handler) {
parent.addEventListener(event, function(e) {
if (e.target.matches(selector)) {
handler.call(e.target, e);
}
});
}
// Usage
delegate(document, '.button', 'click', function(event) {
console.log('Button clicked:', this.textContent);
});
delegate(document, '[data-toggle]', 'click', function(event) {
const target = document.querySelector(this.dataset.toggle);
if (target) {
target.classList.toggle('active');
}
});
DOM Traversal
Methods for navigating the DOM tree.
Walking the DOM
// Walk up the DOM tree
function walkUp(element, callback) {
callback(element);
if (element.parentNode) {
walkUp(element.parentNode, callback);
}
}
// Walk down the DOM tree
function walkDown(element, callback) {
callback(element);
for (let i = 0; i < element.children.length; i++) {
walkDown(element.children[i], callback);
}
}
// Find ancestor by selector
function findAncestor(element, selector) {
let ancestor = element.parentElement;
while (ancestor && !ancestor.matches(selector)) {
ancestor = ancestor.parentElement;
}
return ancestor;
}
// Usage
const item = document.querySelector('.item');
const container = findAncestor(item, '.container');
console.log(container);
Element Searching
// Find closest ancestor
const element = document.querySelector('.item');
const closestDiv = element.closest('div');
// Find matching children
const parent = document.getElementById('parent');
const matchingChildren = parent.querySelectorAll('.item');
// Find matching descendants
const container = document.getElementById('container');
const allItems = container.querySelectorAll('.item');
// Find next/previous matching element
function nextMatching(element, selector) {
let next = element.nextElementSibling;
while (next) {
if (next.matches(selector)) {
return next;
}
next = next.nextElementSibling;
}
return null;
}
// Find elements by text content
function findByText(root, text) {
const walker = document.createTreeWalker(
root,
NodeFilter.SHOW_TEXT,
null,
false
);
const results = [];
let node;
while (node = walker.nextNode()) {
if (node.textContent.includes(text)) {
results.push(node.parentElement);
}
}
return results;
}
Performance Considerations
Optimizing DOM operations for better performance.
Minimize Reflows
// Bad: Multiple reflows
element.style.width = '100px';
element.style.height = '100px';
element.style.backgroundColor = 'red';
element.style.border = '1px solid black';
// Good: Batch style changes
element.style.cssText = 'width: 100px; height: 100px; background-color: red; border: 1px solid black;';
// Better: Use classes
element.className = 'styled-element';
// Best: Use CSS classes and add/remove them
element.classList.add('highlight');
// Batch DOM updates
function updateElements(elements, updates) {
// Remove from DOM
const fragment = document.createDocumentFragment();
// Update in memory
elements.forEach((element, index) => {
const update = updates[index];
if (update.text) element.textContent = update.text;
if (update.className) element.className = update.className;
fragment.appendChild(element);
});
// Single reflow
document.body.appendChild(fragment);
}
Efficient Selection
// Cache DOM queries
const header = document.getElementById('header');
const container = document.getElementById('container');
// Use specific selectors
const specific = document.querySelector('#container .item.active');
// Better than generic
const generic = document.querySelectorAll('.item');
// Use context-specific queries
const containerItems = container.querySelectorAll('.item');
// Better than
const allItems = document.querySelectorAll('.item');
// Use querySelector for single elements
const singleElement = document.querySelector('.item');
// Better than
const firstElement = document.querySelectorAll('.item')[0];
// Use modern methods
const elements = document.querySelectorAll('.item');
elements.forEach(el => console.log(el));
Lazy Loading
// Lazy load images
function lazyLoadImages() {
const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
});
});
images.forEach(img => imageObserver.observe(img));
}
// Lazy load content
function lazyLoadContent() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const element = entry.target;
// Load content
loadContent(element);
observer.unobserve(element);
}
});
});
document.querySelectorAll('[data-lazy]').forEach(el => {
observer.observe(el);
});
}
Modern DOM APIs
Recent additions to the DOM API.
Mutation Observer
// Observe DOM changes
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
console.log('Mutation type:', mutation.type);
console.log('Mutation target:', mutation.target);
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
console.log('Element added:', node);
}
});
}
});
});
// Start observing
observer.observe(document.body, {
childList: true, // Observe child additions/removals
attributes: true, // Observe attribute changes
subtree: true, // Observe descendants
attributeOldValue: true, // Get old attribute values
characterData: true // Observe text changes
});
// Stop observing
observer.disconnect();
// Practical usage: Auto-save form
const formObserver = new MutationObserver(() => {
saveFormData();
});
formObserver.observe(form, {
attributes: true,
subtree: true
});
Resize Observer
// Observe element size changes
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach(entry => {
const { width, height } = entry.contentRect;
console.log(`Element size: ${width}x${height}`);
// Adjust layout based on size
if (width < 600) {
entry.target.classList.add('mobile');
} else {
entry.target.classList.remove('mobile');
}
});
});
// Observe multiple elements
document.querySelectorAll('.responsive').forEach(el => {
resizeObserver.observe(el);
});
// Stop observing
resizeObserver.disconnect();
Intersection Observer
// Observe element visibility
const intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('Element is visible');
entry.target.classList.add('visible');
} else {
console.log('Element is hidden');
entry.target.classList.remove('visible');
}
});
}, {
threshold: 0.5, // Trigger when 50% visible
rootMargin: '10px' // 10px margin around viewport
});
// Observe elements
document.querySelectorAll('.animate-on-scroll').forEach(el => {
intersectionObserver.observe(el);
});
// Infinite scrolling
const sentinel = document.querySelector('#sentinel');
const infiniteObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadMoreContent();
}
});
});
infiniteObserver.observe(sentinel);
Best Practices
Guidelines for writing efficient and maintainable DOM code.
Separation of Concerns
// Keep HTML, CSS, and JavaScript separate
// HTML: Structure
// CSS: Presentation
// JavaScript: Behavior
// Avoid inline event handlers
// Bad: <button onclick="doSomething()">Click</button>
// Good: Separate event handling
document.getElementById('myButton').addEventListener('click', doSomething);
// Use semantic HTML
// Good: <button>Submit</button>
// Bad: <div onclick="submit()">Submit</div>
Error Handling
// Safe DOM operations
function safeQuerySelector(selector) {
try {
return document.querySelector(selector);
} catch (error) {
console.error('Invalid selector:', selector);
return null;
}
}
function safeAddEventListener(element, event, handler) {
if (element && typeof element.addEventListener === 'function') {
element.addEventListener(event, handler);
} else {
console.error('Invalid element or missing addEventListener');
}
}
// Check element existence before manipulation
function manipulateElement(id) {
const element = document.getElementById(id);
if (!element) {
console.warn(`Element with id "${id}" not found`);
return;
}
// Safe to manipulate
element.classList.add('active');
}
Memory Management
// Clean up event listeners
function addCleanupableListener(element, event, handler) {
element.addEventListener(event, handler);
// Return cleanup function
return () => {
element.removeEventListener(event, handler);
};
}
// Usage
const cleanup = addCleanupableListener(button, 'click', handleClick);
// Later: cleanup();
// Remove observers when done
const observer = new MutationObserver(callback);
observer.observe(element, options);
// Cleanup
observer.disconnect();
// Avoid memory leaks in single-page apps
function cleanupPage() {
// Remove all event listeners
document.removeEventListener('*', '*');
// Disconnect all observers
mutationObserver.disconnect();
resizeObserver.disconnect();
intersectionObserver.disconnect();
// Clean up global references
window.currentPage = null;
}
Summary
Key Takeaways
- The DOM represents web pages as a tree of objects
- Use appropriate selection methods for performance
- Event delegation efficiently handles dynamic content
- Modern APIs provide powerful observation capabilities
- Minimize DOM reflows for better performance
Best Practices
- Cache DOM queries and use specific selectors
- Use event delegation for dynamic content
- Batch DOM operations to minimize reflows
- Clean up event listeners and observers
- Separate structure, presentation, and behavior
Common Pitfalls
- Using innerHTML with untrusted content (XSS risk)
- Creating memory leaks with event listeners
- Excessive DOM manipulation causing performance issues
- Not cleaning up observers in single-page apps
- Mixing concerns (inline handlers, styles in JS)