The DOM.
Where JavaScript becomes visible.
The DOM is the browser's in-memory copy of your HTML. Every element you see on screen is a JavaScript object you can read, change, add, or remove. This is the lesson where JavaScript stops being abstract variables and console logs — and starts changing what users actually see. If you've been waiting for the moment it clicks, this is it.
Select elements with document.querySelector('.class') (any CSS selector, first match) or querySelectorAll (all matches). Change content with .textContent (safe, raw text) or .innerHTML (parses HTML — only for trusted content). Change classes with .classList.add(), .remove(), .toggle(). Create elements with document.createElement() and .appendChild(). Remove with .remove(). That's the entire DOM toolkit — everything else builds on these.
Before this lesson: complete Lesson 3. You need to be comfortable with arrow functions and return values.
🎯 Selecting elements
Every DOM operation starts with a reference. Get one, then do things with it.
To change an element, you first need to find it. The browser exposes several methods — modern code uses querySelector for everything because it takes any CSS selector you already know from styling.
// Get ONE element — first match wins
const title = document.querySelector("h1");
const button = document.querySelector("#submit-btn");
const firstCard = document.querySelector(".card");
// Get ALL matching elements — returns a NodeList
const cards = document.querySelectorAll(".card");
// NodeList isn't a real array — convert it to use array methods
Array.from(cards).forEach(card => {
console.log(card.textContent);
});
// Scoped queries — search inside another element
const nav = document.querySelector("nav");
const navLinks = nav.querySelectorAll("a");
// getElementById — faster but only works with IDs
const header = document.getElementById("main-header");
// null check — querySelector returns null if nothing matches
const missing = document.querySelector(".does-not-exist");
console.log(missing); // null
querySelector returns null when nothing matches. It doesn't throw an error — it gives you null. Then when you try el.textContent = ..., you get "Cannot read properties of null". Always check first: if (el) el.textContent = ... — or use optional chaining: el?.textContent = ... . This is the #1 DOM bug.
querySelector
Returns the FIRST matching element or null. Takes any CSS selector: #id, .class, tag, [attr], parent > child.
querySelectorAll
Returns a NodeList of all matches. Not a real array — but works with for...of and .forEach(). Static — doesn't update as DOM changes.
getElementById
Slightly faster for IDs. Returns the element or null. Only takes IDs, no selectors. Modern code uses querySelector('#id') instead.
Scoped queries
nav.querySelectorAll("a") searches only inside nav, not the whole page. Useful when the same class appears in multiple sections.
✏️ Changing content
Three ways to change text. One is safe, one is dangerous, one is for attributes.
const heading = document.querySelector("h1");
// textContent — set raw text (safe)
heading.textContent = "New heading";
// innerHTML — set HTML (parsed)
heading.innerHTML = "bold";
// Read content
console.log(heading.textContent);
console.log(heading.innerHTML);
// Change attributes
const link = document.querySelector("a");
link.href = "https://example.com";
link.setAttribute("target", "_blank");
// Change styles directly (avoid this in production — toggle classes instead)
heading.style.color = "red";
heading.style.fontSize = "32px";
// Reading input values
const input = document.querySelector("input");
console.log(input.value);
input.value = "new value";
Never use innerHTML with user input. If you write el.innerHTML = userInput, an attacker can inject a script tag or an img onerror handler and run arbitrary code in your users' browsers. This is called XSS (cross-site scripting). Rule: user input always goes through textContent. innerHTML is for trusted HTML you wrote yourself.
🏗️ Creating and removing elements
Build new DOM nodes, insert them, remove them. This is how dynamic lists work.
// Create a new element — exists in memory, not on page yet
const newDiv = document.createElement("div");
newDiv.textContent = "I was created by JavaScript";
newDiv.className = "card";
// Append to an existing element
document.body.appendChild(newDiv);
// Or append to a specific parent
const container = document.querySelector("#container");
container.appendChild(newDiv);
// Prepend — insert as first child
container.prepend(newDiv);
// Modern insertAdjacentElement — very flexible
const target = document.querySelector("#target");
target.insertAdjacentElement("beforebegin", newDiv);
target.insertAdjacentElement("beforeend", newDiv);
// Build multiple elements from data — the pattern you'll use constantly
const items = ["Apple", "Banana", "Cherry"];
const ul = document.querySelector("ul");
ul.innerHTML = ""; // clear existing content first
items.forEach(name => {
const li = document.createElement("li");
li.textContent = name;
ul.appendChild(li);
});
// Remove an element
const oldEl = document.querySelector(".old");
oldEl.remove(); // one line, works everywhere
createElement
Creates a new element in memory. It's not on the page until you append it. Set its properties first, then insert.
appendChild
Adds the element as the last child of the parent. Returns the appended child. Classic method — works everywhere.
prepend / append
Modern methods that accept multiple arguments and text nodes. parent.append(el1, el2, "text") adds all at the end.
remove
Modern one-line removal. Older browsers needed parent.removeChild(el). If you support IE11 (you don't), use the parent method.
🎨 Classes — the right way to change styling
Don't set styles directly. Toggle classes. Let CSS do the styling.
The tempting approach is to change styles with JavaScript: el.style.color = "red". But this mixes concerns — your CSS should own all styling, and JavaScript should just toggle classes. When you need dark mode, hover states, or responsive design, CSS is where it belongs. JS toggles the class; CSS handles the effect.
const card = document.querySelector(".card");
// Add a class
card.classList.add("active");
// Add multiple classes at once
card.classList.add("active", "highlighted", "focused");
// Remove a class
card.classList.remove("active");
// Toggle — adds if missing, removes if present
card.classList.toggle("active");
// Toggle with explicit force — true adds, false removes
card.classList.toggle("active", true); // always add
card.classList.toggle("active", false); // always remove
// Check if a class exists
if (card.classList.contains("active")) {
console.log("card is active");
}
// Replace one class with another
card.classList.replace("active", "inactive");
// The pattern: toggle a class, let CSS handle the rest
const menuBtn = document.querySelector("#menu-btn");
const menu = document.querySelector("#menu");
menuBtn.addEventListener("click", () => {
menu.classList.toggle("open");
});
Why classList and not className. className is a string of space-separated classes. To add a class with className, you'd have to check if it's already there, then append carefully — easy to break with double spaces or duplicates. classList is atomic: it handles duplicates, spaces, and checks for you. Always use classList. Always.
💻 Build something — a live to-do list
Every DOM skill in one small example you can actually run.
This is a complete working to-do list. Create an HTML file with this content, open it in a browser, and interact with it. It uses everything from this lesson: selecting, creating, appending, removing, toggling classes, and handling input values.
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: sans-serif; max-width: 400px; margin: 40px auto; }
input { padding: 8px; font-size: 16px; width: 70%; }
button { padding: 8px 16px; cursor: pointer; }
ul { list-style: none; padding: 0; }
li { padding: 10px; margin: 6px 0; background: #f0f0f0; border-radius: 6px; display: flex; justify-content: space-between; cursor: pointer; }
li.done { text-decoration: line-through; opacity: 0.5; }
li span.delete { color: red; cursor: pointer; font-weight: bold; }
</style>
</head>
<body>
<h1>Todo</h1>
<input id="text" placeholder="What needs doing?">
<button id="add">Add</button>
<ul id="list"></ul>
<script>
const input = document.querySelector("#text");
const addBtn = document.querySelector("#add");
const list = document.querySelector("#list");
addBtn.addEventListener("click", () => {
const text = input.value.trim();
if (!text) return;
const li = document.createElement("li");
li.textContent = text;
li.addEventListener("click", (e) => {
if (e.target.classList.contains("delete")) return;
li.classList.toggle("done");
});
const del = document.createElement("span");
del.textContent = "x";
del.className = "delete";
del.addEventListener("click", () => li.remove());
li.appendChild(del);
list.appendChild(li);
input.value = "";
input.focus();
});
</script>
</body>
</html>
This is a real app, not a toy. Every line does something meaningful. The delete button is created separately and appended into the li — that's the pattern for compound elements. The event listener checks e.target.classList.contains("delete") so clicking the delete doesn't also toggle done. The input.value.trim() guards against whitespace-only input.
🔍 What just happened
Why the DOM works the way it does — the mental model.
The DOM is a tree of objects
When the browser parses HTML, it builds a tree in memory where every element is a node. body contains divs which contain paragraphs. When you change a node, the browser marks it as "dirty" and redraws the affected region — usually within 16ms so users see it as instant.
querySelector uses CSS syntax
The same selector language from CSS works in querySelector. That means you don't have to learn a second syntax — if you can style it in CSS, you can select it in JavaScript. This is why document.querySelector("nav ul li.active") works exactly like the CSS rule.
Creating elements takes two steps
document.createElement('div') creates the element but doesn't attach it to the page. It exists in memory only. parent.appendChild(el) attaches it to the live page. The distinction matters — create, configure, then attach. If you attach first, the browser redraws the page for every property change.
Classes control CSS, not JavaScript
When you toggle a class, CSS does the actual visual work. This separation is critical: it means your styling lives in CSS files (easy to find, easy to theme, easy to modify), and your JavaScript just orchestrates state changes. If you ever need to redesign the appearance, you edit CSS only — the JS never changes.
🏆 Challenge
Three tasks. Each one exercises a different DOM skill.
Task 1 — Change the page
Open any website in your browser and use DevTools Console to:
1. Change the main heading's text to "Hello from the console".
2. Change its color to red.
3. Log how many div elements are on the page.
4. Select all links and print their href attributes.
This is the fastest way to internalize DOM selection — doing it on real pages you already know.
Task 2 — Dynamic list from data
Create an array const fruits = ["Apple", "Banana", "Cherry", "Date"]. In your HTML page, write JavaScript that:
1. Selects a ul element.
2. Clears its existing content.
3. Creates an li for each fruit and appends them.
4. Gives each li a class of "fruit".
5. Adds a click listener to each that toggles a "selected" class.
This is the exact pattern you'll use for real data from an API (Lesson 6).
Task 3 — Extend the todo app
Take the todo example above and add these features:
1. A "counter" that shows how many items are incomplete.
2. A "clear completed" button that removes all li.done elements.
3. An "edit" feature — double-click an item to edit its text.
4. An "empty state" message when no items remain.
Each feature is 5-10 lines. Each exercises a different part of the DOM API. Build them one at a time, test each one, move on.
⚠️ Common mistakes
The DOM bugs that catch every beginner.
If your script tag is in head without defer, it runs before the body exists. querySelector returns null. Fix: put the script at the end of body, or use defer on the script tag.
el.innerHTML = userInput allows XSS attacks. Any script tag or img onerror in the input runs. Use textContent for anything a user typed.
You set properties on null and get "Cannot read properties of null". Fix: always check with if (el), or use optional chaining: el?.textContent.
el.className += " active" creates duplicate classes and breaks on spaces. Use classList.add() — it handles all edge cases.
querySelector("button.active") is not the same as querySelector(".active button"). The first is "button with class active", the second is "button inside any active element". CSS selectors are precise.
document.querySelectorAll(".item").map(...) throws "map is not a function". Fix: Array.from(nodes).map(...). Or use forEach which works on NodeList.
el.style.color = "red" creates inline styles that override CSS classes. Toggle a class instead — CSS owns styling, JS owns state.
Adding 100 elements one by one triggers 100 page redraws. For large lists, build the entire structure in a DocumentFragment first, then append once. For small lists (under 50), it doesn't matter.
➡️ What's next
You can change the DOM. Next: make it respond to users.
❓ Frequently asked questions
What is the DOM in JavaScript?
The DOM (Document Object Model) is the browser's in-memory representation of your HTML page. Every HTML element becomes a JavaScript object you can read, modify, add, or remove. Change the node, and the browser instantly redraws the page. This is how every interactive website works.
What is the difference between querySelector and getElementById?
querySelector takes any CSS selector and returns the first match. getElementById takes only an ID and returns the element. Both return null if nothing matches. querySelector is more flexible; getElementById is slightly faster. Modern code uses querySelector for everything.
What is the difference between textContent and innerHTML?
textContent sets raw text — HTML in the string appears as literal characters. innerHTML parses HTML — tags in the string are rendered. Use textContent for user input (prevents XSS), and innerHTML only for trusted HTML you wrote.
How do I create a new element in JavaScript?
Use document.createElement('div') to create the element, set its properties, then append it: const div = document.createElement('div'); div.textContent = 'Hello'; document.body.appendChild(div). The element exists in memory until you append it.
How do I add or remove a CSS class?
Use classList: el.classList.add('active') adds, el.classList.remove('active') removes, el.classList.toggle('active') toggles. Never manipulate className directly with string concatenation — it's error-prone. classList is atomic and safer.
Why does my querySelector return null?
Three common causes: (1) selector doesn't match — check the CSS selector is correct, (2) script runs before HTML is parsed — move script to end of body, (3) element is inside a shadow DOM or iframe. Check the element exists visually — if it does, the timing or scope is wrong.
How do I remove an element from the DOM?
Call el.remove() on the element itself — one line, no parent needed. Alternatively: el.parentNode.removeChild(el) (older style). To hide without removing, set el.style.display = 'none' or add a class that hides it. To empty an element's children, use el.innerHTML = ''.
What is the difference between NodeList and HTMLCollection?
querySelectorAll returns a NodeList (static — doesn't update as DOM changes). getElementsByClassName returns an HTMLCollection (live — updates automatically). Both are array-like but neither is a real array. To use array methods on them, convert: Array.from(nodes). Modern code uses querySelectorAll + Array.from for predictability.