The DOM
The Document Object Model (DOM) is the browser's representation of an HTML page as a tree of objects. JavaScript uses the DOM to read and change anything on the page -- text, styles, attributes, structure, everything.
What is the DOM?
When the browser loads an HTML file, it parses the markup and builds a tree structure:
<!DOCTYPE html>
<html>
<head>
<title>My Page</title>
</head>
<body>
<h1>Hello</h1>
<p>World</p>
</body>
</html>
Becomes this tree:
document
└── html
├── head
│ └── title
│ └── "My Page"
└── body
├── h1
│ └── "Hello"
└── p
└── "World"
Every tag becomes a node (an object) in this tree. JavaScript can access and modify any node through the global
document object.
Selecting elements
querySelector -- select one element
Returns the first element that matches a CSS selector:
// By tag
const heading = document.querySelector("h1");
console.log(heading.textContent);
// By class
const card = document.querySelector(".card");
// By ID
const nav = document.querySelector("#main-nav");
// By attribute
const emailInput = document.querySelector('input[type="email"]');
// Nested selector
const firstLink = document.querySelector("nav a");
If no element matches, querySelector returns null. Always check before using:
const element = document.querySelector(".nonexistent");
if (element) {
console.log(element.textContent);
} else {
console.log("Element not found");
}
Result:
Element not found
querySelectorAll -- select multiple elements
Returns a NodeList of all matching elements:
const paragraphs = document.querySelectorAll("p");
console.log(paragraphs.length);
// Iterate with for...of
for (const p of paragraphs) {
console.log(p.textContent);
}
// Or with forEach
paragraphs.forEach((p, index) => {
console.log(`Paragraph ${index}: ${p.textContent}`);
});
A NodeList is not an array. It supports forEach and for...of, but not map, filter, etc. To use array
methods, convert it:
const items = document.querySelectorAll("li");
const texts = Array.from(items).map((li) => li.textContent);
console.log(texts);
// Alternative: spread operator
const texts2 = [...items].map((li) => li.textContent);
Older selection methods
You will see these in existing code:
// By ID (returns one element)
const header = document.getElementById("header");
// By class name (returns live HTMLCollection)
const cards = document.getElementsByClassName("card");
// By tag name (returns live HTMLCollection)
const divs = document.getElementsByTagName("div");
querySelector and querySelectorAll are more flexible and are the modern standard. Use them.
Modifying text content
textContent
Gets or sets the text of an element (ignores HTML):
<p id="greeting">Hello, <strong>world</strong>!</p>
const p = document.querySelector("#greeting");
// Read
console.log(p.textContent);
// Write (replaces all content, including child elements)
p.textContent = "Goodbye, world!";
console.log(p.textContent);
Result:
Hello, world!
Goodbye, world!
innerHTML
Gets or sets the HTML content (parses HTML tags):
<div id="content">
<p>Original content</p>
</div>
const div = document.querySelector("#content");
// Read
console.log(div.innerHTML);
// Write (replaces content with parsed HTML)
div.innerHTML = "<h2>New Title</h2><p>New paragraph</p>";
Security warning: Never use innerHTML with user input -- it creates a Cross-Site Scripting (XSS) vulnerability:
// DANGEROUS -- never do this
const userInput = '<img src=x onerror="alert(\'hacked\')">';
div.innerHTML = userInput; // executes the attack
// SAFE -- use textContent for user input
div.textContent = userInput; // displays as plain text
Use textContent for plain text. Use innerHTML only with trusted, hardcoded content. For building complex elements
safely, use createElement (covered below).
Modifying attributes
getAttribute / setAttribute
<a id="link" href="https://example.com" target="_blank">Example</a>
const link = document.querySelector("#link");
// Read
console.log(link.getAttribute("href"));
console.log(link.getAttribute("target"));
// Write
link.setAttribute("href", "https://mozilla.org");
console.log(link.getAttribute("href"));
// Remove
link.removeAttribute("target");
console.log(link.getAttribute("target"));
Result:
https://example.com
_blank
https://mozilla.org
null
Direct property access
Many attributes are available as properties:
const link = document.querySelector("#link");
// These are equivalent:
console.log(link.href);
console.log(link.getAttribute("href"));
// For boolean attributes:
const input = document.querySelector("input");
input.disabled = true;
input.required = false;
// For value:
const textInput = document.querySelector('input[type="text"]');
textInput.value = "New value";
console.log(textInput.value);
dataset -- custom data attributes
HTML data-* attributes are accessible via dataset:
<div id="user" data-user-id="42" data-role="admin">Ada</div>
const div = document.querySelector("#user");
console.log(div.dataset.userId); // camelCase conversion
console.log(div.dataset.role);
// Set a new data attribute
div.dataset.status = "active";
// This adds data-status="active" to the HTML
Result:
42
admin
Modifying CSS classes
classList
The modern way to work with CSS classes:
const box = document.querySelector(".box");
// Add a class
box.classList.add("highlighted");
// Remove a class
box.classList.remove("old-style");
// Toggle (add if missing, remove if present)
box.classList.toggle("active");
// Check if a class exists
console.log(box.classList.contains("highlighted"));
// Replace a class
box.classList.replace("highlighted", "selected");
// Add multiple classes
box.classList.add("bold", "large", "rounded");
className
Gets or sets the entire class string (overwrites all classes):
const box = document.querySelector(".box");
console.log(box.className); // "box highlighted"
box.className = "new-class"; // replaces everything
Prefer classList -- it is safer because it does not overwrite existing classes.
Modifying inline styles
const box = document.querySelector(".box");
// Set individual properties (camelCase)
box.style.backgroundColor = "#ff0000";
box.style.padding = "20px";
box.style.borderRadius = "8px";
box.style.fontSize = "18px";
// Read
console.log(box.style.backgroundColor);
// Remove a style
box.style.padding = "";
Note: element.style only reads inline styles. To read computed styles (including CSS file styles):
const box = document.querySelector(".box");
const computed = getComputedStyle(box);
console.log(computed.fontSize);
console.log(computed.color);
For most styling, add/remove CSS classes instead of setting inline styles. It keeps your JavaScript clean and your styles in CSS where they belong.
Creating elements
createElement and appendChild
// Create a new element
const paragraph = document.createElement("p");
paragraph.textContent = "This paragraph was created with JavaScript.";
paragraph.classList.add("dynamic");
// Add it to the page
document.body.appendChild(paragraph);
Building complex structures
function createCard(title, description) {
const card = document.createElement("div");
card.classList.add("card");
const h3 = document.createElement("h3");
h3.textContent = title;
const p = document.createElement("p");
p.textContent = description;
card.appendChild(h3);
card.appendChild(p);
return card;
}
const container = document.querySelector(".grid");
container.appendChild(createCard("Card 1", "First card description"));
container.appendChild(createCard("Card 2", "Second card description"));
insertBefore
Insert an element before a specific child:
const list = document.querySelector("ul");
const newItem = document.createElement("li");
newItem.textContent = "Inserted item";
// Insert before the second child
const secondItem = list.children[1];
list.insertBefore(newItem, secondItem);
insertAdjacentHTML
Insert HTML at specific positions:
const heading = document.querySelector("h1");
// beforebegin -- before the element
heading.insertAdjacentHTML("beforebegin", "<p>Before the heading</p>");
// afterbegin -- inside, at the start
heading.insertAdjacentHTML("afterbegin", "<span>★ </span>");
// beforeend -- inside, at the end
heading.insertAdjacentHTML("beforeend", "<span> ★</span>");
// afterend -- after the element
heading.insertAdjacentHTML("afterend", "<p>After the heading</p>");
Same security warning as innerHTML: do not use with user input.
Removing elements
// Remove a specific element
const element = document.querySelector(".remove-me");
element.remove();
// Remove a child
const list = document.querySelector("ul");
const firstItem = list.firstElementChild;
list.removeChild(firstItem);
// Remove all children
const container = document.querySelector(".container");
container.innerHTML = ""; // quick but creates XSS risk if mixed with user content
// Safer way to clear children
while (container.firstChild) {
container.removeChild(container.firstChild);
}
Cloning elements
const original = document.querySelector(".card");
// Shallow clone (element only, no children)
const shallow = original.cloneNode(false);
// Deep clone (element and all descendants)
const deep = original.cloneNode(true);
document.body.appendChild(deep);
Traversing the DOM
Navigate between related elements:
<ul id="menu">
<li>Home</li>
<li>About</li>
<li>Contact</li>
</ul>
const list = document.querySelector("#menu");
// Children
console.log(list.children.length);
console.log(list.firstElementChild.textContent);
console.log(list.lastElementChild.textContent);
// Parent
const firstItem = list.firstElementChild;
console.log(firstItem.parentElement.id);
// Siblings
const about = list.children[1];
console.log(about.previousElementSibling.textContent);
console.log(about.nextElementSibling.textContent);
// Closest ancestor matching a selector
const item = document.querySelector("li");
const closestUl = item.closest("ul");
console.log(closestUl.id);
Result:
3
Home
Contact
menu
Home
Contact
menu
Element vs Node traversal
| Element property | Node property | Difference |
|---|---|---|
children | childNodes | childNodes includes text nodes and comments |
firstElementChild | firstChild | Same |
parentElement | parentNode | Same |
nextElementSibling | nextSibling | Same |
Use the Element versions (left column) unless you specifically need to work with text nodes.
Document fragments
When adding many elements, using a DocumentFragment avoids repeated reflows:
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const li = document.createElement("li");
li.textContent = `Item ${i + 1}`;
fragment.appendChild(li);
}
// One single DOM update instead of 100
document.querySelector("ul").appendChild(fragment);
Getting element dimensions and position
const box = document.querySelector(".box");
// Size including padding and border
const rect = box.getBoundingClientRect();
console.log(`Width: ${rect.width}, Height: ${rect.height}`);
console.log(`Top: ${rect.top}, Left: ${rect.left}`);
console.log(`Bottom: ${rect.bottom}, Right: ${rect.right}`);
// Size properties
console.log(`offsetWidth: ${box.offsetWidth}`); // width + padding + border
console.log(`clientWidth: ${box.clientWidth}`); // width + padding (no border)
console.log(`scrollHeight: ${box.scrollHeight}`); // total scrollable height
Practical example: building a list from data
<div id="app">
<h1>Users</h1>
<ul id="user-list"></ul>
</div>
const users = [
{ name: "Ada", role: "Admin" },
{ name: "Bob", role: "User" },
{ name: "Charlie", role: "User" },
{ name: "Diana", role: "Moderator" },
];
const list = document.querySelector("#user-list");
for (const user of users) {
const li = document.createElement("li");
const nameSpan = document.createElement("strong");
nameSpan.textContent = user.name;
const roleSpan = document.createElement("span");
roleSpan.textContent = ` -- ${user.role}`;
li.appendChild(nameSpan);
li.appendChild(roleSpan);
list.appendChild(li);
}
This renders:
• Ada -- Admin
• Bob -- User
• Charlie -- User
• Diana -- Moderator
When to use which approach
| Task | Method |
|---|---|
| Set plain text | textContent |
| Set HTML from trusted source | innerHTML (never with user input) |
| Build elements from user data | createElement + textContent |
| Add/remove CSS classes | classList.add() / .remove() / .toggle() |
| Change styles dynamically | Prefer toggling classes; use style for computed values |
| Add many elements at once | DocumentFragment |
Summary
- The DOM is the browser's tree representation of an HTML page.
querySelectorandquerySelectorAllselect elements using CSS selectors.textContentsafely sets text;innerHTMLparses HTML (avoid with user input).classListmanages CSS classes;stylesets inline styles.createElement+appendChildbuilds new elements safely.remove()andremoveChild()delete elements from the page.parentElement,children,closest()navigate the tree.- Use
DocumentFragmentfor batch DOM updates.
Next up: Events & Interactivity -- making your page respond to user actions.