Selecting Elements
querySelector and querySelectorAll let you find elements using the same CSS selector syntax you already know — they return live or static node collections.
- Use querySelector to find the first element matching a CSS selector
- Use querySelectorAll to collect all matching elements and iterate them
- Scope a query to a subtree with element.querySelector
- Explain why querySelector returns null and why you must check for it
- Understand the trade-offs between querySelector and the older collection APIs
Before you can change anything in the DOM, you need a reference to the element
you want to change. The two methods you will use for almost everything are
querySelector and querySelectorAll — both accept the same
CSS selector syntax you already know from
stylesheets.
querySelector — one match
document.querySelector(selector) scans the document and returns the first
element that matches, or null if nothing matches.
// Any valid CSS selector works
const heading = document.querySelector('h1');
const loginBtn = document.querySelector('#login-button');
const firstCard = document.querySelector('.card');
const emailInput = document.querySelector('input[type="email"]');Because it returns null on no match, always check before you use the result:
const banner = document.querySelector('.promo-banner');
if (banner) {
banner.style.display = 'none';
}Calling a method on null throws a TypeError and stops the rest of your
script from running — a common beginner bug.
querySelectorAll — multiple matches
document.querySelectorAll(selector) returns a static NodeList containing
every matching element. "Static" means it captures a snapshot — if the DOM
changes after the call, the NodeList does not update.
Iterate it with forEach or for...of:
const cards = document.querySelectorAll('.card');
// forEach
cards.forEach(card => {
card.classList.add('highlighted');
});
// for...of — identical result, slightly more readable in some cases
for (const card of cards) {
card.classList.add('highlighted');
}A NodeList with zero matches has .length === 0 and is safe to iterate — it
simply does nothing. You do not need a null check before iterating.
A NodeList is not an Array — it is missing array methods like .map,
.filter, and .reduce. Convert it when you need those:
Array.from(document.querySelectorAll('.card')).map(…).
Scoping to a subtree
Both methods also exist on any element, not just document. When you call them
on an element, they search within that element only:
const sidebar = document.querySelector('#sidebar');
// Finds only .card elements inside the sidebar — not the whole page
const sidebarCards = sidebar.querySelectorAll('.card');Scoping is a good habit. It makes the intent explicit, reduces the risk of accidentally selecting an element from a different part of the page, and produces cleaner, more maintainable component-style code.
The older APIs — and why they come second
Before querySelector existed, developers used methods that still work today:
| Method | Returns | Live? |
|---|---|---|
getElementById('id') | Element or null | — |
getElementsByClassName('cls') | HTMLCollection | Yes |
getElementsByTagName('div') | HTMLCollection | Yes |
The getElementsBy* variants return live HTMLCollections — they update
automatically as the DOM changes. That sounds useful until it causes an infinite
loop: modifying the DOM inside a for loop that iterates a live collection can
make the loop run forever or skip elements.
querySelector and querySelectorAll are consistent, static, and accept any
CSS selector. Use them by default; reach for the older APIs only if you are
reading legacy code or need a live collection for a specific reason.
Store references, do not repeat queries
DOM queries are not free — the browser has to walk the tree each time. If you need an element more than once, store it:
// Good — one query, reused
const submitBtn = document.querySelector('#submit');
submitBtn.addEventListener('click', handleSubmit);
submitBtn.setAttribute('aria-busy', 'true');
// Avoid — queries the DOM twice for the same element
document.querySelector('#submit').addEventListener('click', handleSubmit);
document.querySelector('#submit').setAttribute('aria-busy', 'true');For elements that appear on every page load and never change, storing the
reference in a const at the top of your module is the standard pattern.
Where to go next
You can now point at any element in the DOM. The next lesson — Reading and Writing Content — covers what you can actually do with that reference: read text, swap content, toggle classes, and set attributes.