Code of the Day
JavaScript in the BrowserJavaScript in the Browser

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.

Web FoundationsDialect appendices7 min read
Recommended first
By the end of this lesson you will be able to:
  • 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 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:

MethodReturnsLive?
getElementById('id')Element or null
getElementsByClassName('cls')HTMLCollectionYes
getElementsByTagName('div')HTMLCollectionYes

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.

Finished reading? Mark it complete to track your progress.

On this page