Selecting elements
Find nodes with CSS selectors, understand when you get a static list versus a live collection, and scope a search to one subtree.
querySelector and querySelectorAll
const one = document.querySelector('#signup .field'); // first match, or null
const many = document.querySelectorAll('ul.todos > li'); // static NodeList
many.forEach(el => el.classList.add('row')); // a NodeList is iterable
const arr = Array.from(many); // convert when you need array methods
const n = document.querySelectorAll('.card').length;- Selectors are CSS syntax:
.class,#id,[data-role="save"],li:not(.done). querySelectorreturns the first match in document order, ornullwhen nothing matches.querySelectorAllalways returns a NodeList, never a single element; index it or iterate it.- An invalid selector throws
SyntaxErrorimmediately, which is worth remembering when a selector string is assembled from a variable.
Static lists versus live collections
| API | Returns | Updates itself? |
|---|---|---|
querySelectorAll | static NodeList | No |
getElementsByClassName | live HTMLCollection | Yes |
getElementsByTagName | live HTMLCollection | Yes |
document.forms, document.images | live HTMLCollection | Yes |
element.children | live HTMLCollection | Yes |
const live = document.getElementsByClassName('task'); // suppose length is 3
list.append(newTask);
live.length; // 4 - the same object changed under you
// snapshot before mutating, or elements get skipped
for (const el of Array.from(live)) el.remove();
// same query, same result - only the liveness differs
document.querySelectorAll('.task').length; // 3⚠️
Removing nodes while looping a live collection skips matches, because the collection re-indexes on every mutation. Copy it first with
Array.from(), or use querySelectorAll, which is already a snapshot.Scoping a search and walking upwards
// search inside one subtree instead of the whole document
const card = document.querySelector('.card');
card.querySelector('.title'); // only nodes inside this card
card.querySelectorAll('a[href]'); // relative to the card
// walk up from the element you already have
const row = event.target.closest('tr[data-id]');
const form = input.closest('form');
const outsideModal = el.closest('.modal') === null;
// test a single element without searching for it
if (link.matches('a[target="_blank"]')) { /* ... */ }closest()starts at the element itself and walks up through its ancestors.matches()answers a yes/no question about one element; it does not return a list.- Cache a parent reference and query inside it; a full-document query repeated inside a loop or a scroll handler is the usual performance mistake.
- A selector called on an element is still matched against the whole subtree in tree order, so the result order is the document order of the matches.
FAQ
Why does my query return null even though the element exists?
The script ran before the parser reached the element, or the selector is wrong. Load the file with
defer or as a module, and remember that ids are matched as #id rather than as bare text.Is querySelectorAll slow?
A single call is fast enough to use freely. The cost appears when the same query runs on every keystroke or scroll event: cache the parent element and the result, and invalidate the cache only when the DOM actually changes.
Related
Working with the DOM Reading and updating content safely
Last refreshed 2026-09-18.