DOM Manipulation
Reviewed & published by Brayan K
Learn how to interact with and modify HTML elements using JavaScript — bring your webpages to life with dynamic content, styles, and interactivity.
Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- What the DOM is and how it works
- Select elements with querySelector
- Change text content and innerHTML
- Modify styles and CSS classes dynamically
- Create, append, and remove elements
- Navigate the DOM tree with parent/child relationships
💡 Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:
- Download Node.js to run JavaScript on your computer
- Use your browser's Developer Console (Press F12) to test code snippets
- Create a .html file with <script> tags and open it in your browser
🌳 What is the DOM?
The Document Object Model (DOM) is a programming interface for web documents. It represents your HTML page as a tree of objects that JavaScript can interact with.
Think of the DOM as a live representation of your webpage that you can read and modify in real-time.
Imagine your HTML is like a blueprint of a house. The DOM is the actual house that's been built from that blueprint. With JavaScript, you can walk through that house, change the paint colors, move furniture around, or even add new rooms — all while people are inside!
Everything on a webpage — every paragraph, image, button, and div — is represented as an object in the DOM tree.
🎯 Why DOM Manipulation Matters
Without the DOM, web pages would be static and boring. DOM manipulation is what makes the web interactive:
- ✓ Update content without reloading the page (like social media feeds)
- ✓ Respond to user actions (clicks, typing, scrolling)
- ✓ Create animations and visual effects
- ✓ Build dynamic forms with validation
- ✓ Show/hide elements based on conditions
🔍 Selecting Elements — Finding What You Need
Before you can manipulate an element, you need to select it. JavaScript gives you several powerful methods to find elements in the DOM.
📌 Method 1: getElementById
The most direct way to select an element — by its unique id attribute.
const header = document.getElementById('main-header');
console.log(header); // Returns the element with id="main-header"📌 Method 2: getElementsByClassName
Select all elements with a specific class name. Returns an HTMLCollection (array-like object).
const buttons = document.getElementsByClassName('btn');
console.log(buttons[0]); // First button
console.log(buttons.length); // Number of buttons📌 Method 3: querySelector (Modern & Recommended)
The most versatile method — uses CSS selectors to find elements. Returns the first match.
// Select by class
const container = document.querySelector('.container');
// Select by ID
const header = document.querySelector('#main-header');
// Select by tag
const firstParagraph = document.querySelector('p');
// Advanced: select nested elements
const submitBtn = document.querySelector('form .submit-btn');📌 Method 4: querySelectorAll
Like querySelector, but returns all matching elements as a NodeList.
const allButtons = document.querySelectorAll('.btn');
// Loop through all buttons
allButtons.forEach(button => {
console.log(button.textContent);
});Use querySelector and querySelectorAll for modern projects. They're more flexible and work just like CSS selectors you already know!
🧑🍳 Worked Example — the whole toolkit in one run
Here is every idea from this lesson in one short program: selecting, reading, changing content, editing classes, creating an element and removing one. Read the comments first, then run it and match each console line to the line of code that produced it.
One deliberate detail: the whole thing is built inside a <div> that is never added to the page. An element you have created but not inserted still behaves like a normal element — you can query it, edit it, and count its children. That is why the editor's console can tell you everything even though nothing visible changes on screen.
// Everything below happens inside ONE container element that is never added
// to the page, so nothing on screen changes. Every DOM method behaves exactly
// as it would on a live page - the console just lets you watch it.
const container = document.createElement('div');
// ---- 1. Some markup to work with -------------------------------------
container.innerHTML =
'<h2 id="title">Shopping list</h2>' +
'<ul class="list">' +
'<li class="item">Milk</li>' +
'<li class="item done">Bread</li>' +
'<li class="item">Eggs</li>' +
'</ul>';
// ---- 2. querySelector returns the FIRST match, or null ---------------
const title = container.querySelector('#title'); // '#' means "id"
console.log('title text: ' + title.textContent);
const first = container.querySelector('.item'); // '.' means "class"
console.log('first item: ' + first.textContent);
const missing = container.querySelector('.nope'); // nothing matches
console.log('missing is: ' + missing); // null - NOT undefined
// ---- 3. querySelectorAll returns EVERY match ------------------------
const items = container.querySelectorAll('.item');
console.log('found ' + items.length + ' items:');
items.forEach(li => console.log(' - ' + li.textContent));
// A NodeList is not an array. Convert it before using map/filter/reduce:
const texts = Array.from(items).map(li => li.textContent);
console.log('joined: ' + texts.join(', '));
// ---- 4. textContent vs innerHTML ------------------------------------
const note = document.createElement('p');
note.textContent = '<b>not bold</b>'; // stored as TEXT: the tags are shown
console.log('after textContent, innerHTML is: ' + note.innerHTML);
note.innerHTML = '<b>really bold</b>'; // PARSED: a real <b> element appears
console.log('after innerHTML, textContent is: ' + note.textContent);
// ---- 5. classList ---------------------------------------------------
const bread = container.querySelectorAll('.item')[1];
console.log('bread classes: ' + bread.className);
console.log('is it done? ' + bread.classList.contains('done'));
bread.classList.remove('done');
bread.classList.add('urgent');
console.log('after editing: ' + bread.className);
// ---- 6. Create and append -------------------------------------------
const list = container.querySelector('.list');
const li = document.createElement('li'); // exists in memory only...
li.textContent = 'Coffee';
li.classList.add('item');
list.appendChild(li); // ...until it is appended
console.log('items now: ' + container.querySelectorAll('.item').length);
// ---- 7. Remove -------------------------------------------------------
container.querySelector('.item').remove(); // the first .item is Milk
const left = Array.from(container.querySelectorAll('.item')).map(x => x.textContent);
console.log('after remove: ' + left.join(', '));
// ✅ Expected output:
// title text: Shopping list
// first item: Milk
// missing is: null
// found 3 items:
// - Milk
// - Bread
// - Eggs
// joined: Milk, Bread, Eggs
// after textContent, innerHTML is: <b>not bold</b>
// after innerHTML, textContent is: really bold
// bread classes: item done
// is it done? true
// after editing: item urgent
// items now: 4
// after remove: Bread, Eggs, Coffee
//
// Line 3 is the one that catches everyone: a failed querySelector gives you
// null, so 'Cannot read properties of null' almost always means your
// selector matched nothing - a typo, or a '#' where you wanted a '.'.
// Line 8 is the security lesson in one line: textContent escaped the angle
// brackets into <b> instead of creating an element. That is exactly why
// you use textContent for anything a user typed.✏️ Changing Content — textContent vs innerHTML
Once you've selected an element, you can change what's inside it.
🔤 Using textContent (Safer)
Changes the plain text inside an element. HTML tags are treated as text, not rendered.
const heading = document.querySelector('h1');
heading.textContent = "Welcome to my website!";
// HTML tags are escaped (safe from XSS attacks)
heading.textContent = "<script>alert('hack')</script>";
// Displays as text, doesn't run code ✅🏗️ Using innerHTML (More Powerful)
Changes the HTML content inside an element. Tags are parsed and rendered.
const container = document.querySelector('.container');
container.innerHTML = "<h2>New Title</h2><p>Some text</p>";
// Can add multiple elements at once
container.innerHTML = `
<div class="card">
<h3>Card Title</h3>
<p>Card content</p>
</div>
`;Be careful with innerHTML when inserting user-provided content! It can expose your site to XSS (Cross-Site Scripting) attacks. Use textContent for user input, or sanitize the HTML first.
🎨 Changing Styles — Making Things Beautiful
You can modify CSS styles directly using JavaScript's style property.
🖌️ Basic Styling
const box = document.querySelector('.box');
// Change individual properties
box.style.color = "blue";
box.style.backgroundColor = "#f0f0f0";
box.style.fontSize = "20px";
box.style.padding = "15px";
box.style.borderRadius = "8px";🎭 Multiple Styles at Once
const element = document.querySelector('.element');
// Using cssText (overwrites existing styles)
element.style.cssText = `
color: white;
background-color: #333;
padding: 20px;
border-radius: 10px;
`;🔄 Example: Theme Switcher
const body = document.body;
const themeBtn = document.querySelector('#theme-toggle');
themeBtn.addEventListener('click', () => {
if (body.style.backgroundColor === 'white' || !body.style.backgroundColor) {
// Dark mode
body.style.backgroundColor = '#1a1a1a';
body.style.color = '#ffffff';
} else {
// Light mode
body.style.backgroundColor = 'white';
body.style.color = '#000000';
}
});For complex styling, it's better to add/remove CSS classes rather than inline styles. This keeps your styling logic in CSS where it belongs!
🏷️ Working with Classes — The Professional Way
The classList API is the modern, clean way to manage CSS classes on elements.
➕ Adding Classes
const button = document.querySelector('.btn');
// Add a single class
button.classList.add('active');
// Add multiple classes
button.classList.add('primary', 'large', 'animated');➖ Removing Classes
// Remove a single class
button.classList.remove('active');
// Remove multiple classes
button.classList.remove('large', 'animated');🔄 Toggling Classes
The most useful method — adds the class if it's not there, removes it if it is.
const menu = document.querySelector('.menu');
const toggleBtn = document.querySelector('#menu-toggle');
toggleBtn.addEventListener('click', () => {
menu.classList.toggle('open'); // Switches between open/closed
});✅ Checking if a Class Exists
if (element.classList.contains('active')) {
console.log('Element is active!');
}📝 Real-World Example: Tabs
const tabs = document.querySelectorAll('.tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
// Remove 'active' from all tabs
tabs.forEach(t => t.classList.remove('active'));
// Add 'active' to clicked tab
tab.classList.add('active');
});
});- • No need to manually parse/split strings
- • Prevents accidentally removing other classes
- • More readable and maintainable code
🏗️ Creating & Adding Elements — Build on the Fly
JavaScript lets you create brand new HTML elements and insert them into your page dynamically.
🆕 Creating Elements
// Step 1: Create the element
const newDiv = document.createElement('div');
// Step 2: Add content
newDiv.textContent = "I'm a new div!";
// Step 3: Add styling/classes
newDiv.classList.add('card');
newDiv.style.padding = '20px';
// Step 4: Add to the page
document.body.appendChild(newDiv);📦 Inserting Elements — Different Ways
const container = document.querySelector('.container');
const newElement = document.createElement('p');
newElement.textContent = "New paragraph";
// Method 1: Add as last child
container.appendChild(newElement);
// Method 2: Add as first child
container.insertBefore(newElement, container.firstChild);
// Method 3: Modern - insertAdjacentElement
container.insertAdjacentElement('afterbegin', newElement); // Start
container.insertAdjacentElement('beforeend', newElement); // End
container.insertAdjacentElement('beforebegin', newElement); // Before container
container.insertAdjacentElement('afterend', newElement); // After container🗑️ Removing Elements
const element = document.querySelector('.remove-me');
// Modern way
element.remove();
// Old way (still works)
element.parentNode.removeChild(element);📋 Example: To-Do List
const todoList = document.querySelector('#todo-list');
const addButton = document.querySelector('#add-todo');
const input = document.querySelector('#todo-input');
addButton.addEventListener('click', () => {
// Create new list item
const li = document.createElement('li');
li.textContent = input.value;
li.classList.add('todo-item');
// Add delete button
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '×';
deleteBtn.addEventListener('click', () => li.remove());
li.appendChild(deleteBtn);
todoList.appendChild(li);
// Clear input
input.value = '';
});🎯 Your Turn — wire up a small to-do list
The markup and the last few lines are written for you. Three blanks, each one a method you met earlier in this lesson. Fill them in, run it, and check your console against the expected output at the bottom.
// 🎯 YOUR TURN — fill in the blanks marked with ___
const app = document.createElement('div');
app.innerHTML =
'<h1 class="heading">To-do</h1>' +
'<ul id="tasks">' +
'<li class="task">Write lesson</li>' +
'<li class="task">Test the code</li>' +
'<li class="task">Ship it</li>' +
'</ul>';
// 1) Grab the heading. Its class is "heading", so the CSS selector for it
// starts with a dot.
const heading = app.querySelector(___); // 👉 replace ___ with '.heading'
console.log('heading: ' + heading.textContent);
// 2) Grab ALL three tasks in one go. querySelector would give you only the
// first one - you want the plural version.
const tasks = app.___('.task'); // 👉 replace ___ with querySelectorAll
console.log('task count: ' + tasks.length);
// 3) Mark the second task (index 1) as done by ADDING the class 'done'.
tasks[1].classList.___('done'); // 👉 replace ___ with add
console.log('second task classes: ' + tasks[1].className);
// Already written for you: build a fourth task and append it
const li = document.createElement('li');
li.textContent = 'Celebrate';
li.classList.add('task');
app.querySelector('#tasks').appendChild(li);
console.log('task count now: ' + app.querySelectorAll('.task').length);
console.log('last task: ' + app.querySelectorAll('.task')[3].textContent);
// ✅ Expected output once all three blanks are filled:
// heading: To-do
// task count: 3
// second task classes: task done
// task count now: 4
// last task: Celebrate
//
// 'task count: undefined'? Blank 2 returned a single element, not a list.
// ❌ TypeError: Cannot read properties of null means blank 1's selector
// matched nothing - check the dot.
// 'second task classes: task' with no 'done'? Blank 3 is not 'add'.🔧 Working with Attributes — The Details
Attributes are the properties you set in HTML tags, like id, src, href, data-*, etc.
🎯 Getting Attributes
const link = document.querySelector('a');
const img = document.querySelector('img');
// Get attribute values
console.log(link.getAttribute('href'));
console.log(img.getAttribute('src'));
console.log(img.getAttribute('alt'));✏️ Setting Attributes
const button = document.querySelector('button');
// Set attributes
button.setAttribute('disabled', 'true');
button.setAttribute('aria-label', 'Submit form');
button.setAttribute('data-action', 'submit');🗑️ Removing Attributes
button.removeAttribute('disabled');📊 Data Attributes (data-*)
Custom attributes that start with data- are perfect for storing extra information.
// HTML: <div id="user" data-id="123" data-role="admin"></div>
const user = document.querySelector('#user');
// Access via dataset
console.log(user.dataset.id); // "123"
console.log(user.dataset.role); // "admin"
// Set new data attribute
user.dataset.status = "active";
// Creates: data-status="active"🚫 Common Mistakes to Avoid
❌ Mistake 1: Selecting Before DOM is Ready
// ❌ BAD - Script runs before HTML loads
const button = document.querySelector('.btn'); // null!
button.addEventListener('click', ...); // Error!
// ✅ GOOD - Wait for DOM to load
document.addEventListener('DOMContentLoaded', () => {
const button = document.querySelector('.btn');
button.addEventListener('click', ...);
});❌ Mistake 2: Not Checking if Element Exists
// ❌ BAD - Assumes element exists
const header = document.querySelector('.header');
header.textContent = "New title"; // Error if null!
// ✅ GOOD - Check first
const header = document.querySelector('.header');
if (header) {
header.textContent = "New title";
}❌ Mistake 3: Using innerHTML with User Input
// ❌ BAD - XSS vulnerability
const userInput = "<script>alert('hacked')</script>";
element.innerHTML = userInput; // Script runs!
// ✅ GOOD - Use textContent for user input
element.textContent = userInput; // Safe, displays as text❌ Mistake 4: Modifying DOM in a Loop (Slow)
// ❌ BAD - Multiple reflows
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = i;
document.body.appendChild(div); // Triggers reflow each time
}
// ✅ GOOD - Build first, append once
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = i;
fragment.appendChild(div);
}
document.body.appendChild(fragment); // Single reflow✅ Best Practices
- ✓ Use querySelector/querySelectorAll — Most flexible and modern approach
- ✓ Prefer classList over style — Keep styling in CSS, logic in JavaScript
- ✓ Use textContent for user input — Prevents XSS attacks
- ✓ Cache element selections — Store references in variables instead of querying repeatedly
- ✓ Use DocumentFragment for bulk operations — Better performance
- ✓ Always check if elements exist — Prevents null reference errors
- ✓ Use semantic HTML — Makes DOM manipulation easier and more accessible
🧪 Interactive Code Practice
Try out DOM manipulation in this live editor. The code demonstrates selecting, creating, and modifying elements.
🎯 Mini-Challenge: Build a Leaderboard from Data
This is the job you will do more than any other on a real page: turn an array of data into elements. You are given four players and a scoring rule — only players on 50 or more make the board. No blanks this time, just an outline.
Build an ordered list (ol) in memory, one li per qualifying player, and leave it unattached. The three console.log lines at the bottom check your work for you.
const players = [
{ name: 'Ada', score: 92 },
{ name: 'Grace', score: 48 },
{ name: 'Linus', score: 61 },
{ name: 'Bjarne', score: 50 }
];
// 🎯 MINI-CHALLENGE: build the leaderboard
// 1. Create an <ol> element with document.createElement and store it in a
// const called 'board'.
// 2. Loop over players. Skip anyone whose score is below 50.
// 3. For each qualifying player:
// - create an <li>
// - set its textContent to player.name + ' - ' + player.score
// - add the class 'row' with classList.add
// - append it to board with board.appendChild(...)
// 4. Do NOT append board to the page - leave it in memory like the worked
// example did.
// your code here
console.log('rows: ' + board.querySelectorAll('.row').length);
board.querySelectorAll('.row').forEach(li => console.log(' ' + li.textContent));
console.log('html: ' + board.innerHTML);
// ✅ Expected output when your version is right:
// rows: 3
// Ada - 92
// Linus - 61
// Bjarne - 50
// html: <li class="row">Ada - 92</li><li class="row">Linus - 61</li><li class="row">Bjarne - 50</li>
//
// Before you write anything you will get:
// ❌ ReferenceError: board is not defined
// which is the editor telling you step 1 has not happened yet.
// Four rows instead of three means your score test is > 50 rather than >= 50 -
// Bjarne is exactly on the line and belongs on the board.📚 Quick Reference Guide
🔍 Selection Methods
- getElementById(id) — Get element by ID
- querySelector(selector) — First match (CSS selector)
- querySelectorAll(selector) — All matches
- getElementsByClassName() — By class name
- getElementsByTagName() — By tag name
🏗️ Creation Methods
- createElement(tag) — Create new element
- appendChild(element) — Add as last child
- insertBefore(new, ref) — Insert before reference
- removeChild(element) — Remove child element
- element.remove() — Remove element itself
✏️ Content Methods
- textContent — Get/set text (safe)
- innerHTML — Get/set HTML (powerful)
- value — Get/set form input value
- innerText — Get/set visible text
🎨 Style & Class Methods
- element.style.property — Set inline style
- classList.add() — Add CSS class
- classList.remove() — Remove CSS class
- classList.toggle() — Toggle CSS class
- classList.contains() — Check if class exists
🔧 Attribute Methods
- getAttribute(name) — Get attribute value
- setAttribute(name, val) — Set attribute
- removeAttribute(name) — Remove attribute
- hasAttribute(name) — Check if exists
- dataset.property — Access data-* attributes
🔄 Traversal Methods
- parentElement — Get parent element
- children — Get child elements
- firstElementChild — First child
- lastElementChild — Last child
- nextElementSibling — Next sibling
🎯 Practice Challenges
Test your DOM manipulation skills with these challenges:
Challenge 1: Profile Card Creator
Create a function that generates a user profile card with:
- • Profile picture (use an img element)
- • Name and role
- • Background color based on role (admin = blue, user = green)
- • Click event that logs the user's details
Challenge 2: Dynamic Form Validator
Create a form validator that:
- • Selects an input field
- • Adds a red border if input is empty
- • Shows an error message below the field
- • Removes errors when user types
Challenge 3: Color Palette Generator
Build a function that:
- • Creates 5 colored div boxes
- • Each box has a random background color
- • Displays the hex code in the box
- • Clicking a box copies the color code
Challenge 4: Table Row Striping
- • Selects all rows in a table
- • Adds "odd" or "even" class to alternating rows
- • Adds hover effect using classList
- • Makes clicking a row highlight it
Build a mini shopping cart that lets users add items, increase quantity, remove items, and see the total price update in real-time — all using DOM manipulation!
💬 Recap
- ✓ The DOM is a tree representation of your HTML that JavaScript can manipulate
- ✓ Use querySelector/querySelectorAll for modern, flexible element selection
- ✓ textContent is safe for user input, innerHTML is powerful but risky
- ✓ classList is the best way to manage CSS classes dynamically
- ✓ You can create, modify, and delete elements on the fly
- ✓ Always check if elements exist before manipulating them
- ✓ DOM manipulation is what makes web pages interactive and dynamic
DOM manipulation is the bridge between your data and what users see. Master it, and you can build anything — from simple counters to complex web applications!
End of Lesson: DOM Manipulation
📋 Quick Reference — DOM Manipulation
| Task | Code |
|---|---|
| Select element | document.querySelector('.btn') |
| Change text | el.textContent = "Hello" |
| Add class | el.classList.add('active') |
| Create element | document.createElement('div') |
| Remove element | el.remove() |
Lesson Complete — DOM Manipulation!
You can now select, modify, create, and remove HTML elements using JavaScript — making your webpages truly dynamic!
Practice quiz
What does the DOM represent?
- A CSS stylesheet
- A database of users
- Your HTML page as a tree of objects JavaScript can interact with
- The browser's network requests
Answer: Your HTML page as a tree of objects JavaScript can interact with. The Document Object Model represents the page as a live tree of objects you can read and modify.
What does document.querySelector('.box') return?
- The first element matching the CSS selector
- All elements with class box
- An array of nodes
- The element's text
Answer: The first element matching the CSS selector. querySelector returns the first match for a CSS selector, or null if none is found.
Which method returns ALL matching elements as a NodeList?
- getElementById
- querySelector
- getElementByClass
- querySelectorAll
Answer: querySelectorAll. querySelectorAll returns every matching element as a NodeList.
Why is textContent safer than innerHTML for user input?
- It is faster
- It treats input as plain text, preventing XSS
- It renders HTML tags
- It validates emails
Answer: It treats input as plain text, preventing XSS. textContent escapes HTML so injected script tags display as text rather than executing — preventing XSS.
What does element.classList.toggle('open') do?
- Adds the class if absent, removes it if present
- Always adds the class
- Always removes the class
- Renames the class
Answer: Adds the class if absent, removes it if present. toggle adds the class when it is missing and removes it when present.
How do you create a new element in the DOM?
- document.newElement('div')
- document.addElement('div')
- document.createElement('div')
- new Element('div')
Answer: document.createElement('div'). document.createElement('div') creates a new, detached element you then insert into the page.
Which appends a node as the last child of a parent?
- parent.insertBefore(node)
- parent.appendChild(node)
- parent.prepend(node, null)
- parent.remove(node)
Answer: parent.appendChild(node). appendChild adds the node as the last child of the parent element.
What is the modern way to remove an element from the DOM?
- element.delete()
- element.destroy()
- element.hide()
- element.remove()
Answer: element.remove(). element.remove() removes the element itself; the older way is element.parentNode.removeChild(element).
How is the CSS property background-color written in element.style?
- background-color
- backgroundColor
- background_color
- BackgroundColor
Answer: backgroundColor. Hyphenated CSS properties become camelCase in JavaScript, so it is element.style.backgroundColor.
How do you read a data-id attribute from an element?
- element.data.id
- element.getData('id')
- element.dataset.id
- element.attributes.id
Answer: element.dataset.id. Custom data-* attributes are accessed through the dataset property, e.g. element.dataset.id.
Continue this course
- Previous: Checkpoint: Data & Logic
- Next: Events — Respond to user clicks, keypresses, and form submissions
- Quick reference: JavaScript cheat sheet › DOM
- From the blog: Understanding the DOM in JavaScript