JavaScript DOM Manipulation Complete Guide - Master Dynamic Web Pages
Learn DOM manipulation from scratch. Discover how to select, modify, create, and delete elements dynamically with vanilla JavaScript. Build interactive web pages without any framework.
JavaScript DOM manipulation is the superpower that transforms static HTML pages into dynamic, interactive experiences. Every time you click a button that changes color, submit a form without reloading, or watch content appear as you scroll — you’re seeing DOM manipulation in action.
In this complete guide, you’ll master everything from selecting elements to building complex interactive features using pure JavaScript. No frameworks, no libraries — just the raw power of the DOM API.

What is the DOM?
The DOM (Document Object Model) is a programming interface for web documents. It represents the page as a tree of objects, where each HTML element becomes a node you can access and modify with JavaScript.
Think of it this way: your HTML file is a blueprint. The DOM is the actual house built from that blueprint — and JavaScript is the toolset that lets you remodel rooms, add windows, or tear down walls while the house is still standing.
<!-- Your HTML -->
<div id="app">
<h1>Hello World</h1>
<p>This is a paragraph.</p>
</div>
// The DOM sees it as a tree:
// Document
// └── div#app
// ├── h1 → "Hello World"
// └── p → "This is a paragraph."
Every element, attribute, and piece of text becomes a manipulable object. Understanding this tree structure is the foundation of all DOM work.
Selecting Elements
Before you can manipulate anything, you need to find it. JavaScript provides several ways to select DOM elements, each suited to different scenarios.
querySelector — The Swiss Army Knife
querySelector is the most versatile selector. It accepts any CSS selector and returns the first matching element:
// By ID
const header = document.querySelector('#header');
// By class
const cards = document.querySelector('.card');
// By attribute
const input = document.querySelector('input[type="email"]');
// Nested selectors
const title = document.querySelector('main .content h1');
// Data attribute
const modal = document.querySelector('[data-modal="signup"]');
querySelectorAll — For Multiple Elements
When you need all matching elements, use querySelectorAll:
// Returns a NodeList (not an array!)
const paragraphs = document.querySelectorAll('p');
const buttons = document.querySelectorAll('.btn-primary');
// Iterate with forEach
paragraphs.forEach((para, index) => {
para.textContent = `Paragraph ${index + 1}`;
});
// Convert to array for more methods
const allLinks = [...document.querySelectorAll('a')];
allLinks.filter(link => link.href.includes('external'));
Pro tip:
querySelectorAllreturns aNodeList, not an Array. Use[...nodeList]orArray.from()to convert it if you need array methods likemap,filter, orreduce.
Legacy Methods
These older methods still work but are less flexible:
// By ID — fast, returns single element
const hero = document.getElementById('hero-section');
// By class name — returns HTMLCollection
const items = document.getElementsByClassName('list-item');
// By tag name — returns HTMLCollection
const images = document.getElementsByTagName('img');
// By name attribute — mainly for forms
const formData = document.getElementsByName('username');
For modern development, querySelector and querySelectorAll cover 95% of use cases.
Modifying Element Content
Once you’ve selected an element, you can change what’s inside it. There are three main properties, each with different behavior:
textContent — Plain Text Only
Sets or gets the raw text content, ignoring any HTML tags:
const heading = document.querySelector('h1');
// Set text
heading.textContent = 'Welcome to My Site';
// Get current text
console.log(heading.textContent); // "Welcome to My Site"
Best for: Updating text safely, especially with user-generated content.
innerHTML — HTML Allowed
Parses HTML strings and renders them as actual elements:
const container = document.querySelector('.container');
// Insert HTML markup
container.innerHTML = `
<div class="card">
<h3>New Card</h3>
<p>This is rendered as HTML.</p>
</div>
`;
// Read current HTML
console.log(container.innerHTML);
Warning: Never use innerHTML with untrusted user input — it opens XSS vulnerabilities. Always sanitize or use textContent for user data.
innerText — Visible Text Only
Similar to textContent but respects CSS visibility rules:
const hiddenElement = document.querySelector('.hidden-element');
// hiddenElement has display:none in CSS
console.log(hiddenElement.innerText); // "" (empty, because it's hidden)
console.log(hiddenElement.textContent); // "I'm here!" (always returns content)
Quick comparison:
| Property | Renders HTML | Respects CSS | Safe for user input | Performance |
|---|---|---|---|---|
textContent | No | No | ✅ Yes | Fastest |
innerText | No | Yes | ✅ Yes | Medium |
innerHTML | Yes | N/A | ❌ Risky | Slowest |
Changing Styles
You can modify element styles directly through JavaScript using the style property:
Inline Styles
const box = document.querySelector('.box');
// Single style
box.style.backgroundColor = '#3b82f6';
box.style.borderRadius = '12px';
box.style.transform = 'scale(1.1)';
// Note: CSS properties use camelCase in JS
// background-color → backgroundColor
// font-size → fontSize
// margin-top → marginTop
toggleClass — The Cleanest Approach
Instead of inline styles, toggling classes is cleaner and separates concerns:
const menu = document.querySelector('.menu');
// Toggle a class on click
menu.addEventListener('click', () => {
menu.classList.toggle('active');
});
// Add multiple classes
menu.classList.add('visible', 'animated');
// Remove specific class
menu.classList.remove('visible');
// Check if class exists
if (menu.classList.contains('active')) {
console.log('Menu is open!');
}
// Replace one class with another
menu.classList.replace('old-class', 'new-class');
getComputedStyle — Reading Calculated Styles
To read the final computed style (including inherited values):
const box = document.querySelector('.box');
const styles = getComputedStyle(box);
console.log(styles.width); // "300px"
console.log(styles.backgroundColor); // "rgb(59, 130, 246)"
console.log(styles.marginTop); // "16px"
Creating and Removing Elements
Dynamic content creation is where DOM manipulation truly shines. You can build entire sections of a page from JavaScript.
createElement — Building from Scratch
// Create a new element
const newCard = document.createElement('div');
newCard.className = 'product-card';
newCard.id = 'product-42';
// Add content
newCard.innerHTML = `
<img src="/images/product.jpg" alt="Product">
<h3>Amazing Product</h3>
<p class="price">$29.99</p>
<button>Add to Cart</button>
`;
// Append to parent
document.querySelector('.products').appendChild(newCard);
appendChild vs append
Both add elements, but append is more flexible:
const list = document.querySelector('ul');
// appendChild — adds one node
const item = document.createElement('li');
item.textContent = 'Item 1';
list.appendChild(item);
// append — adds nodes OR strings, multiple at once
list.append(
document.createTextNode('Item 2'),
'\n',
document.createTextNode('Item 3')
);
insertBefore — Precise Placement
const list = document.querySelector('ul');
const firstItem = list.children[0];
const newItem = document.createElement('li');
newItem.textContent = 'New First Item';
// Insert before the first child
list.insertBefore(newItem, firstItem);
removeChild and remove
const oldElement = document.querySelector('.obsolete');
// Traditional way
oldElement.parentNode.removeChild(oldElement);
// Modern shorthand (preferred)
oldElement.remove();
Working with Attributes
Attributes provide metadata about elements — IDs, classes, URLs, data values, and more.
Setting and Getting Attributes
const link = document.querySelector('a');
// Set attributes
link.setAttribute('href', 'https://example.com');
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
// Get attributes
console.log(link.getAttribute('href')); // "https://example.com"
// Check if attribute exists
if (link.hasAttribute('target')) {
console.log('Opens in new tab');
}
// Remove attribute
link.removeAttribute('target');
dataset — Accessing data-* Attributes
The dataset property gives clean access to custom data attributes:
<div data-user-id="123" data-role="admin" data-signup-date="2024-01-15">
User Profile
</div>
const userDiv = document.querySelector('[data-user-id]');
// Access via camelCase property names
console.log(userDiv.dataset.userId); // "123"
console.log(userDiv.dataset.role); // "admin"
console.log(userDiv.dataset.signupDate); // "2024-01-15"
// Set data attributes
userDiv.dataset.lastActive = new Date().toISOString();
Note:
data-*attributes become properties ondatasetwith hyphens removed and the next letter capitalized (data-signup-date→dataset.signupDate).
Event Handling
Events are the heartbeat of interactivity. They respond to user actions — clicks, keypresses, scrolls, and more.
addEventListener — The Standard Approach
const button = document.querySelector('#submit-btn');
button.addEventListener('click', function(event) {
event.preventDefault(); // Prevent default form submission
const formData = new FormData(this.closest('form'));
console.log(Object.fromEntries(formData));
this.textContent = 'Submitting...';
this.disabled = true;
});
Common Events Reference
| Event | Triggered When |
|---|---|
click | User clicks/taps the element |
dblclick | User double-clicks |
mouseenter | Cursor enters the element |
mouseleave | Cursor leaves the element |
keydown / keyup | Keyboard key pressed/released |
submit | Form is submitted |
change | Input value changes (select, checkbox) |
input | Input value changes in real-time |
scroll | Parent element is scrolled |
resize | Browser window is resized |
load | Page or image finishes loading |
DOMContentLoaded | HTML is fully parsed |
Event Object — Rich Context
Every event handler receives an event object with useful information:
document.addEventListener('keydown', (event) => {
console.log(event.key); // "Enter", "Escape", "a", etc.
console.log(event.code); // "Enter", "Escape", "KeyA", etc.
console.log(event.ctrlKey); // true if Ctrl is held
console.log(event.shiftKey); // true if Shift is held
console.log(event.altKey); // true if Alt is held
console.log(event.target); // The element that triggered the event
console.log(event.type); // "keydown"
});
Event Delegation — One Listener, Many Elements
Instead of attaching listeners to every individual element, attach one to a parent:
// BAD: Attaching listener to every button
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', () => deleteItem(btn.dataset.id));
});
// GOOD: Single listener on parent
document.querySelector('.items-list').addEventListener('click', (event) => {
const deleteBtn = event.target.closest('.delete-btn');
if (deleteBtn) {
deleteItem(deleteBtn.dataset.id);
}
});
Event delegation is especially powerful for lists, tables, and any content that changes dynamically.
Traversing the DOM Tree
Sometimes you need to navigate from one element to its relatives rather than selecting from the root.
Parent and Children
const card = document.querySelector('.card');
// Parent
const section = card.parentElement;
const article = card.closest('article'); // Closest ancestor matching selector
// Direct children
const title = card.firstElementChild;
const lastItem = card.lastElementChild;
// All children
const children = card.children; // HTMLCollection
for (let i = 0; i < children.length; i++) {
console.log(children[i].tagName);
}
Siblings
const activeItem = document.querySelector('.active');
// Next sibling (any node type)
const nextAny = activeItem.nextSibling;
// Next sibling (element only)
const nextElement = activeItem.nextElementSibling;
// Previous sibling
const prevElement = activeItem.previousElementSibling;
// All siblings
const siblings = [
...activeItem.parentElement.children
].filter(el => el !== activeItem);
Finding Related Elements
const label = document.querySelector('label');
// Find the associated input
const input = label.htmlFor
? document.getElementById(label.htmlFor)
: label.nextElementSibling;
// Find the closest form
const form = input.closest('form');
// Find all inputs within that form
const allInputs = form.querySelectorAll('input');
Real-World Examples
Let’s put everything together with practical examples you might encounter in real projects.
Example 1: Interactive Accordion
class Accordion {
constructor(container) {
this.container = container;
this.headers = container.querySelectorAll('.accordion-header');
this.headers.forEach(header => {
header.addEventListener('click', () => this.toggle(header));
});
}
toggle(header) {
const panel = header.nextElementSibling;
const isOpen = panel.classList.contains('open');
// Close all panels
this.container.querySelectorAll('.accordion-panel').forEach(panel => {
panel.classList.remove('open');
panel.style.maxHeight = null;
});
// Open clicked panel if it was closed
if (!isOpen) {
panel.classList.add('open');
panel.style.maxHeight = panel.scrollHeight + 'px';
}
}
}
// Initialize
document.querySelectorAll('.accordion').forEach(acc => {
new Accordion(acc);
});
Example 2: Dynamic Search Filter
function setupSearchFilter(inputSelector, itemsSelector) {
const input = document.querySelector(inputSelector);
const items = document.querySelectorAll(itemsSelector);
input.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase().trim();
items.forEach(item => {
const text = item.textContent.toLowerCase();
const matches = text.includes(query);
item.style.display = matches ? '' : 'none';
item.classList.toggle('highlight', matches && query.length > 0);
});
// Show count
const visibleCount = [...items].filter(i => i.style.display !== 'none').length;
document.querySelector('.result-count').textContent =
`${visibleCount} result${visibleCount !== 1 ? 's' : ''} found`;
});
}
setupSearchFilter('#search-input', '.searchable-item');
Example 3: Infinite Scroll Loader
function setupInfiniteScroll(loadMoreFn) {
let isLoading = false;
window.addEventListener('scroll', () => {
if (isLoading) return;
const scrollBottom = window.innerHeight + window.scrollY;
const docHeight = document.documentElement.scrollHeight;
// Load more when user is near the bottom
if (scrollBottom >= docHeight - 500) {
isLoading = true;
showLoadingIndicator();
loadMoreFn().then(() => {
isLoading = false;
hideLoadingIndicator();
}).catch(() => {
isLoading = false;
hideLoadingIndicator();
});
}
});
}
setupInfiniteScroll(fetchNextPage);
Example 4: Modal Dialog System
function createModal(title, content) {
// Create modal overlay
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.innerHTML = `
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div class="modal-header">
<h2 id="modal-title">${title}</h2>
<button class="modal-close" aria-label="Close">×</button>
</div>
<div class="modal-body">${content}</div>
<div class="modal-footer">
<button class="btn-secondary close-modal">Cancel</button>
<button class="btn-primary confirm-modal">Confirm</button>
</div>
</div>
`;
document.body.appendChild(overlay);
// Close handlers
const closeModal = () => overlay.remove();
overlay.querySelector('.modal-close').addEventListener('click', closeModal);
overlay.querySelector('.close-modal').addEventListener('click', closeModal);
// Close on overlay click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) closeModal();
});
// Close on Escape key
document.addEventListener('keydown', function handler(e) {
if (e.key === 'Escape') {
closeModal();
document.removeEventListener('keydown', handler);
}
});
// Focus trap
overlay.querySelector('.modal').focus();
return {
confirm: new Promise(resolve => {
overlay.querySelector('.confirm-modal').addEventListener('click', () => {
closeModal();
resolve(true);
});
}),
cancel: new Promise(resolve => {
overlay.addEventListener('click', function handler(e) {
if (e.target === overlay) {
resolve(false);
overlay.removeEventListener('click', handler);
}
});
})
};
}
// Usage
createModal('Delete Item', '<p>Are you sure? This cannot be undone.</p>')
.then(confirmed => {
if (confirmed) deleteItem();
});
Performance Best Practices
DOM manipulation can be slow if done inefficiently. Follow these guidelines for optimal performance:
Batch DOM Writes
Reading and writing to the DOM triggers layout recalculations. Minimize these operations:
// BAD: Reading and writing in a loop (triggers reflow each iteration)
items.forEach(item => {
const width = item.offsetWidth; // READ
item.style.width = width + 'px'; // WRITE
});
// GOOD: Batch reads, then batch writes
const widths = items.map(item => item.offsetWidth);
items.forEach((item, i) => {
item.style.width = widths[i] + 'px'; // All writes happen after reads
});
Use DocumentFragment
When adding many elements, use a DocumentFragment to minimize reflows:
// BAD: Appending each item individually (many reflows)
fruits.forEach(fruit => {
const li = document.createElement('li');
li.textContent = fruit;
list.appendChild(li);
});
// GOOD: Build in fragment, append once (single reflow)
const fragment = document.createDocumentFragment();
fruits.forEach(fruit => {
const li = document.createElement('li');
li.textContent = fruit;
fragment.appendChild(li);
});
list.appendChild(fragment);
Cache References
Don’t query the DOM repeatedly — store references:
// BAD: Querying DOM every time
function updateStats() {
document.querySelector('.count').textContent = getCount();
document.querySelector('.total').textContent = getTotal();
document.querySelector('.average').textContent = getAverage();
}
// GOOD: Cache references once
const stats = {
count: document.querySelector('.count'),
total: document.querySelector('.total'),
average: document.querySelector('.average')
};
function updateStats() {
stats.count.textContent = getCount();
stats.total.textContent = getTotal();
stats.average.textContent = getAverage();
}
Debounce Scroll and Resize
Heavy events like scroll and resize fire dozens of times per second. Throttle or debounce them:
function debounce(fn, delay = 250) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
window.addEventListener('scroll', debounce(() => {
// Expensive scroll calculations
updateScrollPosition();
}, 100));
Common Pitfalls and How to Avoid Them
Pitfall 1: Script Runs Before DOM Loads
// BAD: Element doesn't exist yet
const button = document.querySelector('#myButton');
button.addEventListener('click', handleClick);
// GOOD: Wait for DOM to be ready
document.addEventListener('DOMContentLoaded', () => {
const button = document.querySelector('#myButton');
button.addEventListener('click', handleClick);
});
// Or place script at end of body
// Or use defer attribute: <script src="app.js" defer></script>
Pitfall 2: Memory Leaks from Event Listeners
// BAD: Adding listeners without cleanup
function initComponent(element) {
element.addEventListener('click', handleClick);
// Component destroyed but listener remains
}
// GOOD: Store reference for cleanup
function initComponent(element) {
const handler = (e) => handleClick(e);
element.addEventListener('click', handler);
return () => {
element.removeEventListener('click', handler);
};
}
const cleanup = initComponent(element);
// Later: cleanup();
Pitfall 3: Using innerHTML with User Input
// BAD: XSS vulnerability
userComment.innerHTML = userInput;
// GOOD: Use textContent for user data
userComment.textContent = userInput;
// If you must use HTML, sanitize it first
import DOMPurify from 'dompurify';
userComment.innerHTML = DOMPurify.sanitize(userInput);
DOM Manipulation vs Frameworks
Understanding vanilla DOM manipulation makes you a better framework developer. Here’s how the concepts map:
| Vanilla DOM | React Equivalent |
|---|---|
querySelector | useRef |
createElement + appendChild | JSX <div> |
textContent / innerHTML | {variable} |
classList.toggle | Conditional className |
addEventListener | onClick={handler} |
removeChild | Conditional rendering |
DocumentFragment | Fragments <>...</> |
When you understand what frameworks do under the hood, you make better architectural decisions and debug issues faster.
Practice Exercises
Try these exercises to solidify your skills:
- Todo List: Build a todo app with add, complete, and delete functionality
- Dark Mode Toggle: Create a theme switcher that persists preference in localStorage
- Image Gallery: Build a gallery with lightbox preview and keyboard navigation
- Form Validator: Create real-time validation with visual feedback
- Drag and Drop: Implement a sortable list using drag events
Each exercise combines multiple DOM techniques and mirrors real-world patterns.
Conclusion
DOM manipulation is the bridge between static HTML and dynamic, interactive web applications. Whether you’re building a simple toggle button or a complex dashboard, the skills covered in this guide form the foundation of everything you’ll build.
Master these fundamentals, and frameworks will feel intuitive rather than magical. You’ll understand not just how to build interfaces, but why they behave the way they do.
Start small — pick one technique from this guide and apply it to your next project. Then layer on more skills as you grow comfortable. Before long, you’ll be creating rich, responsive experiences that delight users and impress colleagues.
Happy coding! 🚀