2 min read
On this page

Selecting & Modifying Elements

The Document Object Model

The DOM is a tree representation of an HTML document. Every element becomes a node. JavaScript interacts with the page by reading and modifying these nodes.

document
  └── html
      ├── head
      └── body
          ├── header
          │   └── h1 ("My Site")
          └── main
              └── p ("Hello world")

Selecting Elements

querySelector & querySelectorAll

querySelector returns the first match. querySelectorAll returns all matches as a static NodeList.

const heading = document.querySelector("h1");
const nav = document.querySelector(".main-nav");
const email = document.querySelector("#email");
const submitBtn = document.querySelector('button[type="submit"]');

const items = document.querySelectorAll(".item");
items.forEach(item => console.log(item.textContent));

Forget getElementById, getElementsByClassName, and getElementsByTagName. querySelector does the same thing with a consistent API.

Scoped Queries

Call querySelector on any element to search within its descendants.

const form = document.querySelector("#signup-form");
const emailInput = form.querySelector('input[name="email"]');

Creating Elements

const list = document.querySelector("#todo-list");
const item = document.createElement("li");
item.textContent = "Buy groceries";
item.classList.add("todo-item");
list.appendChild(item);

append is newer and accepts multiple arguments and strings:

container.append(heading, paragraph, "trailing text");

insertBefore & insertAdjacentHTML

const list = document.querySelector("ul");
const ref = list.querySelector("li:nth-child(2)");
list.insertBefore(newItem, ref);

// Positions: "beforebegin", "afterbegin", "beforeend", "afterend"
section.insertAdjacentHTML("afterbegin", "<p>First paragraph</p>");

Removing Elements

element.remove();                        // modern way
parent.removeChild(child);               // older way
container.replaceChildren();             // clear all children

Modifying Content

textContent vs innerHTML

textContent is safe. innerHTML parses HTML and is dangerous with user input.

heading.textContent = "New Title";                    // safe
container.innerHTML = "<p><strong>Bold</strong></p>"; // parses HTML

// DANGEROUS: XSS vulnerability
const userInput = '<img src=x onerror="alert(document.cookie)">';
container.innerHTML = userInput;   // executes the attack
container.textContent = userInput; // renders as harmless text

Never use innerHTML with user input.

Modifying Classes

const card = document.querySelector(".card");
card.classList.add("highlighted");
card.classList.remove("hidden");
card.classList.toggle("expanded");
card.classList.contains("highlighted");  // true
card.classList.replace("old", "new");
card.classList.add("bold", "large", "primary");  // multiple at once

Modifying Attributes

link.setAttribute("href", "https://example.com");
console.log(link.getAttribute("href"));
input.removeAttribute("disabled");

// Direct property access for common attributes
input.value = "new value";
input.disabled = true;
img.src = "/images/photo.jpg";

Data Attributes

<div id="user" data-user-id="42" data-role="admin"></div>
const el = document.querySelector("#user");
console.log(el.dataset.userId);   // "42" (camelCase)
el.dataset.active = "true";       // sets data-active="true"

Modifying Styles

Inline styles have the highest specificity. Prefer toggling classes instead.

// Inline styles (use sparingly)
box.style.backgroundColor = "coral";
box.style.width = "200px";

// Preferred: toggle a class
card.classList.add("highlighted");

// Read computed styles
const styles = getComputedStyle(box);
console.log(styles.width);

Cloning Elements

const shallow = template.cloneNode(false);  // element only
const deep = template.cloneNode(true);      // element + descendants
document.querySelector("#cards").appendChild(deep);

Cloning is useful when you have a template element you want to reuse multiple times. The clone is independent of the original.

Working with the HTML Template Element

The <template> element holds markup that is not rendered until cloned.

<template id="card-template">
  <div class="card">
    <h3 class="card-title"></h3>
    <p class="card-body"></p>
  </div>
</template>
const template = document.querySelector("#card-template");
const clone = template.content.cloneNode(true);
clone.querySelector(".card-title").textContent = "New Card";
clone.querySelector(".card-body").textContent = "Card content here.";
document.querySelector("#cards").appendChild(clone);

Common Pitfalls

Using innerHTML with user-provided data. This is an XSS vulnerability. Use textContent for text and createElement for dynamic HTML.

Querying the DOM in a loop. Cache references outside the loop.

// Bad: queries on every iteration
for (let i = 0; i < 100; i++) {
  document.querySelector("#list").appendChild(createItem(i));
}
// Good: cache the reference
const list = document.querySelector("#list");
for (let i = 0; i < 100; i++) list.appendChild(createItem(i));

Confusing textContent with innerText. innerText triggers a reflow to compute visibility. Use textContent.

querySelectorAll returns a NodeList, not an Array. It supports forEach but not map or filter. Convert with Array.from(items).

Key Takeaways

  • Use querySelector and querySelectorAll for all element selection.
  • Create elements with createElement and add them with append or appendChild.
  • Use textContent for safe text updates. Never put user input into innerHTML.
  • Manage classes through classList: add, remove, toggle, contains.
  • Prefer toggling classes over setting inline styles.
  • Custom data attributes are accessed via the dataset property.