Skip to content

DOM Manipulation

💡
Before you start

You need a web browser and a plain text editor — nothing else. No Node, no web server, no internet connection: everything below runs from two files sitting in a folder on your own computer. You do not need to know HTML in advance either, because step 1 gives you the page to work on, line for line. If you have never written any JavaScript at all, do Getting Started with JavaScript first — about fifteen minutes.

What is the DOM?

The Document Object Model (DOM) is a programming interface for web pages. When a browser loads an HTML page, it creates a tree-like structure of objects representing every element on the page. JavaScript can interact with this tree to read, modify, add, or remove elements in real time.

Think of the DOM as a live blueprint of your webpage. Every HTML tag becomes a "node" in the tree, and JavaScript gives you the tools to manipulate any node you want.

<!DOCTYPE html>
<html>
  <head>
    <title>My Page</title>
  </head>
  <body>
    <h1>Hello</h1>
    <p>Welcome to my site.</p>
  </body>
</html>

// DOM tree structure:
// document
//   └── html
//       ├── head
//       │   └── title ("My Page")
//       └── body
//           ├── h1 ("Hello")
//           └── p ("Welcome to my site.")

The document object is the entry point to the DOM. Every method you use to find or modify elements starts from document.

Selecting Elements

Before you can change anything on a page, you need to select the element you want to work with. JavaScript provides several methods for finding elements in the DOM.

getElementById

Selects a single element by its id attribute. Returns null if no match is found.

// HTML: <h1 id="main-title">Welcome</h1>

const title = document.getElementById("main-title");
console.log(title.textContent); // "Welcome"

querySelector

Selects the first element that matches a CSS selector. This is the most versatile selection method.

// Select by ID
const title = document.querySelector("#main-title");

// Select by class
const firstCard = document.querySelector(".card");

// Select by tag
const firstParagraph = document.querySelector("p");

// Complex selectors work too
const navLink = document.querySelector("nav a.active");
const input = document.querySelector('input[type="email"]');

querySelectorAll

Selects all elements matching a CSS selector. Returns a NodeList (similar to an array).

const allCards = document.querySelectorAll(".card");
console.log(allCards.length); // Number of matching elements

// Loop through results
allCards.forEach(card => {
    console.log(card.textContent);
});

// You can also use index access
const firstCard = allCards[0];
const lastCard = allCards[allCards.length - 1];
💡
querySelector vs getElementById

querySelector is more flexible because it accepts any CSS selector, while getElementById only works with IDs. However, getElementById is slightly faster in performance-critical situations. For most use cases, querySelector is the preferred modern approach.

Other Selection Methods

// Select by class name (returns HTMLCollection)
const items = document.getElementsByClassName("item");

// Select by tag name (returns HTMLCollection)
const paragraphs = document.getElementsByTagName("p");

// Note: HTMLCollection is live — it updates automatically
// when the DOM changes. NodeList from querySelectorAll is static.

Modifying Content

Once you have selected an element, you can read or change its content.

textContent

Gets or sets the text content of an element, ignoring any HTML tags:

// HTML: <p id="info">Hello <strong>world</strong></p>

const info = document.querySelector("#info");

// Reading
console.log(info.textContent); // "Hello world"

// Writing — replaces everything with plain text
info.textContent = "Goodbye world";
// Result: <p id="info">Goodbye world</p>

innerHTML

Gets or sets the HTML content of an element, including tags:

const info = document.querySelector("#info");

// Reading
console.log(info.innerHTML); // "Hello <strong>world</strong>"

// Writing — parses HTML tags
info.innerHTML = "Goodbye <em>world</em>";
// Result: <p id="info">Goodbye <em>world</em></p>
⚠️
innerHTML and XSS Security Risk

Never insert user-provided data directly into innerHTML. If a user submits text containing <script> tags or other malicious HTML, it will be executed on the page. This is called a Cross-Site Scripting (XSS) attack. Use textContent for user input, or sanitize the HTML first.

Modifying Attributes

const link = document.querySelector("a");

// Read an attribute
console.log(link.getAttribute("href"));

// Set an attribute
link.setAttribute("href", "https://example.com");
link.setAttribute("target", "_blank");

// Remove an attribute
link.removeAttribute("target");

// Check if an attribute exists
console.log(link.hasAttribute("href")); // true

// Common shorthand properties
const img = document.querySelector("img");
img.src = "/images/photo.jpg";
img.alt = "A beautiful photo";

Changing Styles and Classes

Inline Styles

Use the style property to set inline CSS on an element:

const box = document.querySelector(".box");

// Set individual styles (use camelCase for CSS properties)
box.style.backgroundColor = "#1a1a2e";
box.style.color = "#6E8BFF";
box.style.padding = "20px";
box.style.borderRadius = "8px";
box.style.fontSize = "18px";

// Read a style
console.log(box.style.color); // "#6E8BFF"

CSS Classes (Preferred Approach)

Instead of setting styles directly, it is better to add or remove CSS classes. This keeps your styling in CSS files and your logic in JavaScript.

const button = document.querySelector(".btn");

// Add a class
button.classList.add("active");

// Remove a class
button.classList.remove("active");

// Toggle a class (add if missing, remove if present)
button.classList.toggle("active");

// Check if a class exists
if (button.classList.contains("active")) {
    console.log("Button is active");
}

// Replace one class with another
button.classList.replace("btn-primary", "btn-secondary");

// Add multiple classes
button.classList.add("rounded", "shadow", "animated");

Creating and Removing Elements

Creating Elements

Use document.createElement() to build new elements from scratch:

// Create a new paragraph
const newParagraph = document.createElement("p");
newParagraph.textContent = "This paragraph was created by JavaScript!";
newParagraph.classList.add("dynamic-text");

// Create a new link
const newLink = document.createElement("a");
newLink.href = "https://example.com";
newLink.textContent = "Visit Example";
newLink.setAttribute("target", "_blank");

Adding Elements to the Page

const container = document.querySelector("#content");

// Add as the last child
container.appendChild(newParagraph);

// Add as the first child
container.prepend(newLink);

// Insert before a specific element
const reference = document.querySelector("#reference-element");
container.insertBefore(newParagraph, reference);

// Modern methods with more flexibility
container.append(newParagraph, newLink);       // Add multiple at end
container.before(newParagraph);                // Add before the container itself
container.after(newParagraph);                 // Add after the container itself

// Insert HTML at specific positions
container.insertAdjacentHTML("beforebegin", "<p>Before container</p>");
container.insertAdjacentHTML("afterbegin", "<p>First child</p>");
container.insertAdjacentHTML("beforeend", "<p>Last child</p>");
container.insertAdjacentHTML("afterend", "<p>After container</p>");

Removing Elements

// Modern way: element removes itself
const oldElement = document.querySelector("#old-item");
oldElement.remove();

// Classic way: parent removes child
const parent = document.querySelector("#list");
const child = document.querySelector("#list-item-3");
parent.removeChild(child);

// Remove all children from an element
const container = document.querySelector("#content");
container.innerHTML = ""; // Quick but destroys event listeners

// Safer way to remove all children
while (container.firstChild) {
    container.removeChild(container.firstChild);
}

Cloning Elements

const original = document.querySelector(".card");

// Shallow clone (element only, no children)
const shallowCopy = original.cloneNode(false);

// Deep clone (element + all children)
const deepCopy = original.cloneNode(true);

// Add the clone to the page
document.querySelector("#card-list").appendChild(deepCopy);

Traversing the DOM

DOM traversal means navigating between elements using their relationships — parent, child, and sibling connections.

// Given this HTML:
// <ul id="menu">
//   <li>Home</li>
//   <li class="active">About</li>
//   <li>Contact</li>
// </ul>

const active = document.querySelector(".active");

// Parent
console.log(active.parentElement);          // <ul id="menu">

// Children (from the parent)
const menu = document.querySelector("#menu");
console.log(menu.children);                 // HTMLCollection of 3 <li> elements
console.log(menu.children[0]);              // First <li> (Home)
console.log(menu.firstElementChild);        // First <li> (Home)
console.log(menu.lastElementChild);         // Last <li> (Contact)

// Siblings
console.log(active.previousElementSibling); // <li>Home</li>
console.log(active.nextElementSibling);     // <li>Contact</li>

// Closest ancestor matching a selector
const listItem = document.querySelector("li");
const closestList = listItem.closest("ul"); // Finds the nearest <ul> ancestor

Practical Example: Dynamic List

Let us build a complete interactive list application that combines all the DOM techniques covered in this tutorial.

// HTML structure:
// <div id="app">
//   <input type="text" id="item-input" placeholder="Add item...">
//   <button id="add-btn">Add</button>
//   <ul id="item-list"></ul>
//   <p id="count">Items: 0</p>
// </div>

// Select elements
const input = document.querySelector("#item-input");
const addBtn = document.querySelector("#add-btn");
const list = document.querySelector("#item-list");
const count = document.querySelector("#count");

// Function to update the item count
function updateCount() {
    const items = list.querySelectorAll("li");
    count.textContent = `Items: ${items.length}`;
}

// Function to add a new item
function addItem() {
    const text = input.value.trim();
    if (text === "") return;

    // Create the list item
    const li = document.createElement("li");
    li.classList.add("list-item");

    // Create the text span
    const span = document.createElement("span");
    span.textContent = text;

    // Create a delete button
    const deleteBtn = document.createElement("button");
    deleteBtn.textContent = "Delete";
    deleteBtn.classList.add("delete-btn");

    // When delete is clicked, remove the item
    deleteBtn.addEventListener("click", function() {
        li.remove();
        updateCount();
    });

    // Assemble and add to list
    li.appendChild(span);
    li.appendChild(deleteBtn);
    list.appendChild(li);

    // Clear input and update count
    input.value = "";
    input.focus();
    updateCount();
}

// Add item on button click
addBtn.addEventListener("click", addItem);

// Also add item when Enter is pressed
input.addEventListener("keydown", function(event) {
    if (event.key === "Enter") {
        addItem();
    }
});

This example demonstrates element selection, creation, modification, event handling, and removal — the core building blocks of any interactive web application.

Now Do It Yourself: Five Steps

You will build one small page and then change it from JavaScript: find an element, change many at once, create a new one, and finish with the single most important safety rule in this whole subject. Two files in one folder, opened straight from your disk — no server and no internet.

One honest note before you start. Everywhere else on this site the “you should see” lines were captured by running the code on the machine where the page was written. That was not possible here, because the DOM only exists inside a browser and no browser could be opened there. What you get instead: every snippet below was syntax-checked, the two error messages quoted are the real wording from V8 — the engine Chrome, Edge and Node all share — and the visible results are ordinary, standard DOM behaviour. If your browser ever disagrees with a line below, trust your browser.

1
Build a page you can experiment on

Go: make a new folder somewhere convenient and open your text editor. On Windows use Notepad, on macOS TextEdit (with Format → Make Plain Text), on Linux whatever your desktop provides.

Do: save these lines as dom-demo.html inside that folder. In the Save dialog set Save as type to All Files first, or you will silently get dom-demo.html.txt.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>DOM practice</title>
  <style>
    body { font-family: sans-serif; margin: 2rem; }
    .done { text-decoration: line-through; color: grey; }
  </style>
</head>
<body>
  <h1 id="title">Shopping list</h1>
  <ul id="list">
    <li>bread</li>
    <li>milk</li>
  </ul>
  <p id="count"></p>
  <script src="dom.js" defer></script>
</body>
</html>

Do: save an empty file called dom.js beside it, then double-click dom-demo.html.

You should see: a page with the heading Shopping list and two bullet points, and an address bar beginning file:/// rather than http. That prefix is the proof it is your own file: nothing here touches a network. Press F12 to open the developer tools and click Console — leave it open for the rest of this page.

If not: if the browser displays the HTML as plain text instead of a page, the file was saved as .txt. Rename it so that .html is genuinely the last part of the name — Windows hides known extensions by default, so turn on File name extensions in Explorer’s View menu before you judge.

2
Find one element and change it

Go: open dom.js in your editor.

Do: save these three lines as the whole of dom.js, then go back to the browser and reload with F5.

const title = document.getElementById("title");
console.log(title.textContent);
title.textContent = "Today's shopping";

You should see: the heading now reads Today’s shopping, and the console prints Shopping list — the text as it was before the change, because the log ran first. Nothing on disk changed: dom-demo.html still says “Shopping list”. You edited the live page in memory, which is what the DOM is.

If not: TypeError: Cannot set properties of null (setting 'textContent') means getElementById found nothing and handed back null. Two causes, in order of likelihood: the id is spelled differently in the two files, or the defer is missing from the script tag — without it the script runs before the elements exist and every lookup is null. (Firefox words that error differently, but means the same thing.) If instead you tried node dom.js in a terminal you get ReferenceError: document is not defined: there is no document outside a browser, and this file only makes sense inside one.

3
Change many elements at once

Go: back to dom.js.

Do: save these four lines as the whole of dom.js, then reload.

const items = document.querySelectorAll("#list li");
console.log(items.length);
items.forEach(li => li.classList.add("done"));
document.getElementById("count").textContent = `${items.length} items`;

You should see: 2 in the console, both bullet points struck through in grey, and the empty paragraph now reading 2 items. The strike-through comes from the .done rule already in the page’s stylesheet — JavaScript only added the class name. That split is worth keeping: styles belong in CSS, and JavaScript decides which of them apply.

If not: if you reach for items.map(...) you get TypeError: items.map is not a function. querySelectorAll returns a NodeList, which looks like an array and is not one: it has forEach but none of the rest. Turn it into a real array with [...items] when you need map or filter.

4
Create a new element and put it on the page

Go: back to dom.js.

Do: save these four lines at the end of dom.js, keeping step 3’s four lines above them, then reload.

const li = document.createElement("li");
li.textContent = "coffee";
document.getElementById("list").appendChild(li);
console.log(document.querySelectorAll("#list li").length);

You should see: a third bullet, coffee, at the bottom of the list — and it is not struck through, while the first two still are. The console prints 2 and then 3. Read that difference twice, because it explains a whole category of bug: step 3 styled the items that existed at that moment, and an element created afterwards was never part of that group. Anything you add later, you must handle later.

If not: if nothing appears, the element was created but never attached. createElement alone builds an element in mid-air; only appendChild (or append, or insertBefore) puts it into the page. If it appears in the wrong place, check which element you called appendChild on — the new node goes inside that one, at the end.

5
The safety rule: use textContent for anything a person typed

Go: back to dom.js one last time.

Do: save these four lines as the whole of dom.js, then reload.

const typedByAUser = '<img src=x> <b>Free money</b>';
const entry = document.createElement("li");
entry.textContent = typedByAUser;
document.getElementById("list").appendChild(entry);

You should see: a third bullet showing that text literally — angle brackets, tag names and all. No broken-image icon, no bold lettering, nothing built. textContent treats whatever you give it as text, always, however much it looks like markup.

If not: change that one word to innerHTML and reload. The browser stops reading the string as text and builds it: a real image element that fails to load, and real bold lettering. Now picture the same string carrying an event-handler attribute instead — innerHTML wires those up too, and the browser runs the attacker’s code inside your page. That is cross-site scripting, in one word’s difference, and on a real site the string arrives from a comment box, a username or a URL rather than from you. Default to textContent; reach for innerHTML only for markup you wrote yourself, never for anything that passed through a user.

🎉
Check yourself before moving on

Without scrolling up: a colleague’s script logs TypeError: Cannot set properties of null on a page whose markup plainly contains the element. What are the two likely causes, and which would you check first? Answer: the id in the script does not match the id in the HTML, or the script runs before the element exists because the tag has no defer. Check the spelling first — it takes two seconds and is the more common of the two.

Now do it without the page: add a fourth item to the list whose text is today’s date, and give it the done class as you create it. You already have every piece: create, set textContent, classList.add, append — and new Date().toDateString() for the date.

Summary

  • The DOM is a tree-like representation of an HTML page that JavaScript can manipulate
  • Use querySelector and querySelectorAll to find elements using CSS selectors
  • Modify text with textContent (safe) or innerHTML (renders HTML but has XSS risks)
  • Manage CSS classes with classList.add(), .remove(), and .toggle()
  • Create new elements with document.createElement() and add them with appendChild()
  • Remove elements with element.remove()
  • Traverse the DOM using parentElement, children, and sibling properties
🎉
DOM mastery unlocked!

You can now read, modify, create, and remove any element on a web page. Next up: Event Handling — learn how to make your pages respond to user interactions like clicks, keypresses, and form submissions.