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
querySelectorandquerySelectorAllfor all element selection. - Create elements with
createElementand add them withappendorappendChild. - Use
textContentfor safe text updates. Never put user input intoinnerHTML. - Manage classes through
classList:add,remove,toggle,contains. - Prefer toggling classes over setting inline styles.
- Custom data attributes are accessed via the
datasetproperty.