webdev.complete
🌳 The DOM & Events
JavaScript
Lesson 34 of 117
25 min

Selecting & Manipulating

querySelector, classList, dataset, traversal.

The DOM is the live, in-memory tree of nodes that represents your HTML page. Every <div>, every attribute, every bit of text is a node. JavaScript can read, change, add, and remove nodes, and the browser repaints accordingly. This lesson covers the essentials you'll use every day.

Finding elements

js
// By id (fastest, returns one element or null)
const header = document.getElementById("page-header");

// CSS selector (most flexible, returns first match or null)
const firstBtn = document.querySelector(".btn");
const navItem  = document.querySelector("nav > ul > li");

// All matches (returns a static NodeList)
const allBtns = document.querySelectorAll(".btn");

allBtns.forEach(btn => btn.classList.add("ready"));
When to use which
Use getElementById when you know the id (fast and intent-clear). Use querySelector / querySelectorAll for everything else. The CSS-selector version is the modern default.

Reading and writing content

js
const el = document.querySelector("#message");

// Read
el.textContent     // plain text inside the element
el.innerHTML       // HTML markup inside (parsed when set)

// Write
el.textContent = "Hello, world";
el.innerHTML   = "<strong>Hello</strong>, world";
textContent vs innerHTML: XSS
Setting innerHTML from any user input is a classic cross-site scripting hole. Strings with <script> or event handlers will execute. Default to textContent. Reach for innerHTMLonly when you fully trust the source (or you've sanitized it).

Classes, attributes, data

js
const el = document.querySelector(".card");

// Classes
el.classList.add("active");
el.classList.remove("loading");
el.classList.toggle("open");
el.classList.contains("active");   // true

// Attributes (the raw HTML attribute)
el.getAttribute("href");
el.setAttribute("href", "/new-url");
el.removeAttribute("disabled");

// data-* attributes get auto-camelCased into .dataset
// <div data-user-id="42">  →  el.dataset.userId === "42"
el.dataset.userId = "99";          // sets data-user-id="99"

// Inline styles (object form of CSS)
el.style.color = "red";
el.style.backgroundColor = "papayawhip";

Creating and inserting nodes

js
// 1. Build a node
const li = document.createElement("li");
li.textContent = "New item";
li.classList.add("todo");

// 2. Insert it somewhere
const list = document.querySelector("#todos");
list.appendChild(li);                    // at the end
list.prepend(li);                        // at the start (modern)
list.insertBefore(li, list.firstChild);  // old-school version of prepend

// Bulk insert from a string (parsed once)
list.insertAdjacentHTML(
  "beforeend",
  "<li class='todo'>Another one</li>"
);

// Move or remove
li.remove();             // pop it out
otherList.appendChild(li); // moving is a single append

Traversal

js
const el = document.querySelector(".card");

el.parentElement      // parent (skips text nodes)
el.children           // HTMLCollection of element children
el.firstElementChild
el.lastElementChild
el.previousElementSibling
el.nextElementSibling

// Need an ancestor matching a selector?
el.closest(".container")    // walks up until it matches
.closest is gold
Use .closest(selector)whenever you need to find a wrapping element. It's the cleanest way to handle event delegation, scoped lookups, and "which row was clicked" questions.

Document fragments: batch inserts

Inserting nodes one by one in a tight loop triggers a layout pass each time. A DocumentFragment is a temporary container that lets you build a tree off-screen and insert it once:

js
const frag = document.createDocumentFragment();

for (const item of items) {
  const li = document.createElement("li");
  li.textContent = item.label;
  frag.appendChild(li);
}

document.querySelector("#list").appendChild(frag);  // one layout pass

Try it: live DOM manipulation

The playground below has a small todo list. Add items with the input, click an item to mark it done, click the X to remove it. Open it, then change the styling, add a counter, whatever you want.

const list   = document.getElementById("list");
const input  = document.getElementById("input");
const addBtn = document.getElementById("add");

function addTodo(text) {
  if (!text.trim()) return;

  const li = document.createElement("li");
  li.dataset.id = Date.now();
  li.innerHTML =
    "<span class='label'></span>" +
    "<button class='remove' aria-label='remove'>X</button>";

  // Set the label safely with textContent (no XSS)
  li.querySelector(".label").textContent = text;

  list.appendChild(li);
  input.value = "";
  input.focus();
}

addBtn.addEventListener("click", () => addTodo(input.value));
input.addEventListener("keydown", e => {
  if (e.key === "Enter") addTodo(input.value);
});

// Event delegation: one listener for all current and future items
list.addEventListener("click", e => {
  const li = e.target.closest("li");
  if (!li) return;

  if (e.target.classList.contains("remove")) {
    li.remove();
  } else {
    li.classList.toggle("done");
  }
});

// Seed a couple
addTodo("Learn the DOM");
addTodo("Build something");

Quiz

Quiz1 / 3

What's the safest way to set user-provided text in an element?

Recap

  • getElementById for ids, querySelector(All) for everything else.
  • textContent by default. innerHTML only for trusted content. XSS lurks here.
  • classList, dataset, and style are the modern way to mutate elements.
  • createElement + appendChild / prepend / insertAdjacentHTML for building nodes.
  • .closest is the most underrated DOM method. Use it for delegation and scoped lookups.
Built with Next.js, Tailwind & Sandpack.
Learn. Build. Ship.