π DOM Element Selection Methods
Walking the tree node by node gets old fast. The browser gives you a handful of methods to jump straight to any element β by ID, by class, by tag, or by any CSS selector you can dream up. This lesson shows you all five, when to use each, and the subtle live-vs-static difference in what they return.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Use the five core selection methods:
getElementById,getElementsByClassName,getElementsByTagName,querySelector, andquerySelectorAll - Explain the difference between a live HTMLCollection and a static NodeList
- Write powerful CSS selectors for
querySelectorfamily methods - Scope a search to a specific element to search less of the DOM
- Convert collections to real arrays to use
map,filter, and friends
Estimated Time: 25β35 minutes β’ Difficulty: BeginnerβIntermediate
Hands-on: Build a live product filter using selectors and data attributes.
In This Lesson
Why Select Instead of Traverse?
In the previous lesson you moved between nodes using relationships β parent, child, sibling. That is perfect when you start from a known element. But often you just want a specific element anywhere on the page, and you don't want to write a chain of hops to reach it. Selection methods let you describe what you want and hand the search to the browser's highly optimized engine.
π‘ Analogy: Traversal is walking the shelves of a library counting doors. Selection is asking the librarian: "the book with catalog number 12" (an ID), "everything in the mystery genre" (a class), or "the first hardcover about JavaScript on the second floor" (a CSS selector). You describe the target; they fetch it.
The Classic Methods
These three predate CSS selectors and are still perfectly good β especially getElementById, which is the fastest lookup in the DOM.
getElementById() β one element by its unique id
// HTML: <div id="header">Header</div>
const header = document.getElementById('header');
console.log(header); // the element, or null if no match
Note: no # β you pass the bare id. Returns a single element or null.
getElementsByClassName() β a live collection by class
// HTML: <li class="item">β¦</li> <li class="item">β¦</li>
const items = document.getElementsByClassName('item');
console.log(items.length); // 2 β an HTMLCollection
getElementsByTagName() β a live collection by tag
const paragraphs = document.getElementsByTagName('p');
console.log(paragraphs.length); // every <p> on the page
β οΈ These return an HTMLCollection, not an array
An HTMLCollection has a length and numeric indexing, but no forEach, map, or filter. It is also live (see below). To use array methods, convert it first with Array.from(items) or [...items].
querySelector & querySelectorAll
The modern workhorses. Both accept any CSS selector, so a single call can express what used to take several. Reach for these by default.
querySelector() β the first match
// First paragraph that has class "intro"
const intro = document.querySelector('p.intro');
// First <li> inside any <ul>
const firstItem = document.querySelector('ul li');
// First email input on the page
const email = document.querySelector('input[type="email"]');
Returns the first matching element or null.
querySelectorAll() β every match
// All highlighted paragraphs
const highlights = document.querySelectorAll('p.highlight');
// Direct-child list items of ordered lists only
const items = document.querySelectorAll('ol > li');
// Every checked checkbox inside a form
const checked = document.querySelectorAll('form input[type="checkbox"]:checked');
// NodeList HAS forEach built in
checked.forEach(box => console.log(box.name));
Returns a static NodeList. Unlike an HTMLCollection, a NodeList has a built-in forEach, which makes it noticeably more pleasant to loop over.
Live vs. Static Collections
This is the subtlety that catches people. The classic methods return a live collection that keeps updating as the DOM changes; querySelectorAll returns a static snapshot taken at the moment you called it.
| HTMLCollection (getElementsByβ¦) | NodeList (querySelectorAll) | |
|---|---|---|
| Updates when DOM changes? | Yes β live | No β static snapshot |
Has forEach? | No | Yes |
Array methods (map/filter)? | Only after conversion | Only after conversion |
| Convert to array | Array.from(coll) or [...coll] | |
Why does "live" matter? Because a live collection can change length while you loop over it β a classic bug:
// BUG: 'items' is live. Removing an element shrinks it mid-loop,
// so you skip elements and don't remove them all.
const items = document.getElementsByClassName('item');
for (let i = 0; i < items.length; i++) {
items[i].remove(); // items.length drops each time!
}
// FIX: take a static snapshot first
const snapshot = [...document.getElementsByClassName('item')];
snapshot.forEach(el => el.remove()); // reliable
β Practical guidance
Default to querySelectorAll for a stable snapshot you can forEach immediately. Reach for a live getElementsByClassName only when you genuinely want a self-updating count (for example, "how many .selected rows exist right now?").
CSS Selector Cheat Sheet
The power of querySelector is the selector language. Any selector that works in a stylesheet works here:
| Selector | Matches | Example |
|---|---|---|
#id | element with that id | #header |
.class | elements with that class | .item |
tag | all elements of a type | p |
tag.class | type + class together | p.intro |
a > b | direct children only | ul > li |
a b | any descendant | article p |
a, b | either selector | h1, h2, h3 |
[attr="v"] | attribute equals value | input[type="email"] |
:pseudo | state or position | li:nth-child(even) |
// Combine freely β every even .song inside #playlist
const evenSongs = document.querySelectorAll('#playlist li.song:nth-child(even)');
// Disabled buttons inside forms within the dashboard
const disabled = document.querySelectorAll('.dashboard form button[disabled]');
π Handy pseudo-classes
:first-child / :last-child β position among siblings.
:nth-child(2) / :nth-child(even) β the Nth, or every even/odd, sibling.
:checked, :disabled, :required β form-control state.
:not(.hidden) β everything that doesn't match.
Scoping the Search
Every selection method exists not only on document but on any element. Call it on an element and the search is limited to that element's subtree β less DOM to scan, and less risk of grabbing the wrong match elsewhere on the page.
const sidebar = document.getElementById('sidebar');
// Only links inside the sidebar, not the whole page
const sidebarLinks = sidebar.querySelectorAll('a');
// Only .active elements within the sidebar
const active = sidebar.getElementsByClassName('active');
π Library analogy: Searching fromdocumentis scanning the entire library. Scoping tosidebarfirst is walking to one section and only searching those shelves β faster, and you won't accidentally pull a book from the wrong room.
π‘ Working with what you selected
Once you have elements, convert a collection to an array to unlock the full toolkit:
const paragraphs = document.querySelectorAll('p');
// NodeList: forEach works directly
paragraphs.forEach(p => p.classList.add('body-text'));
// Need map/filter? Convert to an array first
const long = [...paragraphs].filter(p => p.textContent.length > 100);
const texts = Array.from(paragraphs, p => p.textContent);
Worked Example: A Product Filter
Here is a realistic, complete feature: a category filter for a product grid. It shows off querySelectorAll, attribute selectors, scoping, and looping β the everyday bread and butter of selection.
<div class="filters">
<button class="filter-btn active" data-category="all">All</button>
<button class="filter-btn" data-category="electronics">Electronics</button>
<button class="filter-btn" data-category="clothing">Clothing</button>
</div>
<div class="products">
<div class="product" data-category="electronics">Laptop</div>
<div class="product" data-category="clothing">T-shirt</div>
<div class="product" data-category="electronics">Headphones</div>
</div>
const buttons = document.querySelectorAll('.filter-btn');
buttons.forEach(btn => {
btn.addEventListener('click', () => {
const category = btn.dataset.category;
// 1. Update which button looks active
document.querySelector('.filter-btn.active')?.classList.remove('active');
btn.classList.add('active');
// 2. Show or hide each product
document.querySelectorAll('.product').forEach(product => {
const show = category === 'all' || product.dataset.category === category;
product.hidden = !show;
});
});
});
Result
Clicking Electronics keeps the Laptop and Headphones visible and hides the T-shirt. Clicking All shows everything again. No page reload, no server call β pure selection and a boolean.
Notice btn.dataset.category reads the data-category attribute, and the optional chaining ?. guards against there being no active button yet. Small touches that keep the code robust.
Hands-on Exercise
ποΈ Build a Searchable Gallery
Objective: Combine several selection methods into one small interactive feature.
Instructions:
- Create a grid of items, each a
<div class="card">with adata-tagsattribute (e.g.data-tags="nature travel") and a title. - Add a text input. On every
inputevent, read its value withquerySelector('#search').value. - Loop the cards with
querySelectorAll('.card').forEach(...)and setcard.hiddenbased on whether the title or tags include the search text. - Show a live count of visible cards, and a "No results" message when the count is zero.
π‘ Hint
Lowercase both sides before comparing: card.dataset.tags.toLowerCase().includes(query.toLowerCase()). Track the visible count by starting a counter at 0 and incrementing it each time you leave a card visible.
β Sample solution β the filter core
const input = document.querySelector('#search');
const cards = document.querySelectorAll('.card');
const status = document.querySelector('#status');
input.addEventListener('input', () => {
const q = input.value.trim().toLowerCase();
let visible = 0;
cards.forEach(card => {
const haystack = (card.textContent + ' ' + card.dataset.tags).toLowerCase();
const match = haystack.includes(q);
card.hidden = !match;
if (match) visible++;
});
status.textContent = visible === 0
? 'No results'
: `${visible} item(s)`;
});
Notice the NodeList is captured once, outside the handler β no need to re-select the cards on every keystroke.
π― Quick Quiz
Question 1: Which method returns a static collection that has a built-in forEach?
Question 2: You want the first <input> of type email inside a form with id signup. Which call is correct?
Question 3: Why can removing elements in a for loop over getElementsByClassName('x') skip some of them?
Summary & Quiz
π Key Takeaways
getElementByIdis the fastest single-element lookup; it takes a bare id and returns one element ornull.getElementsByClassName/TagNamereturn live HTMLCollections with noforEach.querySelector/querySelectorAllaccept any CSS selector; the latter returns a static NodeList withforEach.- Live collections can change length mid-loop β snapshot with
[...coll]before mutating. - Call any method on an element to scope the search to its subtree.
π Further Reading
- MDN β Document.querySelector()
- MDN β Document.querySelectorAll()
- The Modern JavaScript Tutorial β Searching the DOM
π What's Next?
Selecting an element is step one. Next we'll combine selection with traversal β using closest(), contains(), and relationship properties together to navigate from a found element to exactly the neighbor you need.
π You can find anything now!
Five ways to reach any element on the page. Next, let's move around once you're there.