Selecting DOM Elements
Introduction
You know the DOM is made of objects. Now you need to find them. Beginners often throw selectors at the problem until one sticks—then the page layout changes and everything breaks.
This lesson teaches you to write selectors that work reliably. You'll understand why some snap under pressure and others don't, and you'll defend your code against the most common failure: querying for something that doesn't exist.
Learning Objectives
By the end of this lesson, you'll be able to:
- Use CSS selectors to find single and multiple elements
- Choose the right selector method for the job
- Handle the "element not found" case without crashing
- Recognize when selectors are fragile and fix them
Core Concepts
Three Main Selector Methods
document.getElementById() — Find one element by ID (fastest, most specific):
const header = document.getElementById("main-header");
document.querySelector() — Find the first match using CSS selectors:
const firstButton = document.querySelector("button");
const formInput = document.querySelector("input[type='email']");
const cardTitle = document.querySelector(".card .title");
document.querySelectorAll() — Find all matches (returns a NodeList):
const allButtons = document.querySelectorAll("button");
const allRequired = document.querySelectorAll("input[required]");
// Loop through them
allButtons.forEach((btn) => {
btn.addEventListener("click", handleClick);
});
CSS Selectors You'll Use Most
// By tag
document.querySelector("button");
// By class
document.querySelector(".btn");
document.querySelector(".btn.primary"); // both classes
// By ID
document.querySelector("#header");
// By attribute
document.querySelector("input[type='email']");
document.querySelector("[data-user-id='42']");
// Combinations
document.querySelector(".card .title"); // .title inside .card
document.querySelector("button.primary"); // <button class="primary">
The Data Attribute Hook
Instead of relying on class names (which can change for styling), use data-* attributes as stable anchors:
<div class="card rounded bg-white p-4 shadow" data-product-id="42">
<h3 class="text-lg font-bold">Coffee Grinder</h3>
<button class="btn bg-blue-500">Add to Cart</button>
</div>
// Fragile (breaks if classes change):
const addBtn = document.querySelector(".card .btn");
// Stable (explicit intent):
const addBtn = document.querySelector('[data-product-id="42"] .btn');
NodeList Is Not an Array
querySelectorAll() returns a NodeList, which looks like an array but isn't:
const buttons = document.querySelectorAll("button");
// This works (NodeList has forEach):
buttons.forEach((btn) => console.log(btn));
// This breaks (NodeList has no map):
buttons.map((btn) => btn.textContent); // ❌ NodeList has no map method
// Solution: convert to array
const btnArray = Array.from(buttons);
btnArray.map((btn) => btn.textContent); // ✅ Works
The Null Check
Most common beginner crash:
// Selector returns null if element doesn't exist
const button = document.querySelector(".missing-button");
button.addEventListener("click", handleClick); // ❌ Error: button is null
Always guard:
const button = document.querySelector(".missing-button");
if (button) {
button.addEventListener("click", handleClick); // ✅ Safe
} else {
console.log("Button not found—check your selector");
}
// Or use optional chaining (modern browsers):
button?.addEventListener("click", handleClick); // ✅ Also safe
Hands-On Practice
Lab 1: Selector Scavenger Hunt
Create an HTML file with:
<header id="main-header">
<h1>My Site</h1>
<nav class="navbar">
<a href="/">Home</a>
<a href="/about">About</a>
<a href="https://external.com">External</a>
</nav>
</header>
<main>
<form data-form-type="login">
<input type="email" placeholder="Email" required />
<input type="password" placeholder="Password" required />
<button type="submit" class="btn primary">Log In</button>
</form>
<div class="card" data-product-id="1">
<h3 class="card-title">Product A</h3>
<p class="price">$19.99</p>
</div>
<div class="card" data-product-id="2">
<h3 class="card-title">Product B</h3>
<p class="price">$29.99</p>
</div>
</main>
Tasks (write JavaScript to do each):
- Select the main header by ID and log it.
- Select all links inside
.navbarusingquerySelectorAlland log how many there are. - Select internal links (those starting with
/) and append" [internal]"to their text. - Select all required inputs and add a yellow outline (
style.outline = "2px solid gold"). - Select all
.cardelements, loop through them, and add adata-viewed="true"attribute to each. - Select the product with
data-product-id="2"and change its price to"$24.99".
Guard against null on at least one selector (try targeting something that doesn't exist to practice the guard).
Lab 2: Refactor for Stability
You have this brittle selector:
const title = document.querySelector("main .card .card-title");
Refactor it to use a data attribute instead. Show both versions in a comment.
Assessment
Part 1: Complete Both Labs
Do Lab 1 and Lab 2 as written. Your code should run without errors and correctly manipulate the HTML.
Part 2: Reflection (Video or selectors-reflection.md)
Answer these questions concretely:
- In Lab 1, which task initially confused you most? Walk through what you tried, what broke, and how you fixed it. Show code.
- Show your refactored selector from Lab 2. Explain why the new one is more stable than the original.
- When did
Array.from()become necessary? Why couldn't you use the NodeList directly? - Describe one debugging step you took that helped you figure out a broken selector—what did you check, and what did you learn?
- When would you use a data attribute instead of a class name? Give a concrete example from your labs.
Grading criteria:
- Labs run without errors and correctly manipulate the HTML
- At least one selector uses a data attribute defensively
- Code includes null guards on at least one query
- Reflection proves you debugged real problems (not generic answers)
Key Takeaways
- Use
querySelectorfor flexibility,getElementByIdwhen you have an ID. - Data attributes are stable hooks for JavaScript—use them freely.
- NodeList ≠ Array—convert with
Array.from()if you need array methods. - Always check for null before calling methods on queried elements.
- Shorter, intentional selectors outlive fragile chains.