querySelector() in JavaScript: finding an element with a CSS selector

querySelector hands back the first element matching a CSS selector: the frozen list of querySelectorAll, local searches and the null that breaks everything.
3 min read
Believemy logo

A stylesheet already names the elements of a page with a compact syntax: a dot for a class, a hash for an identifier, a space for descendants.

The DOM reuses that exact syntax. Knowing how to write a CSS selector is therefore enough to reach any element from JavaScript.


Definition

querySelector takes a CSS selector as a string and hands back the first element in the document that matches it, in the order the HTML appears. When nothing matches, it hands back null.

JAVASCRIPT
const heading = document.querySelector("h1");
const button = document.querySelector("#cart .confirm");
const firstActive = document.querySelector("li.active");

console.log(heading.textContent);
console.log(document.querySelector(".missing"));   // null

The method exists on document and on any element. Any valid selector goes through, including pseudo-classes such as :first-child or attribute selectors like [data-id="12"].


querySelectorAll and its frozen list

The querySelectorAll variant hands back every matching element inside a NodeList. That list is a snapshot: it never changes again, even when the page changes right after.

JAVASCRIPT
const rows = document.querySelectorAll("#cart li");

rows.forEach((r) => r.classList.add("seen"));
console.log(rows.length);

const labels = Array.from(rows).map((r) => r.textContent);

A NodeList owns forEach and length, but it is not an Array: no map, no filter, no reduce. Converting it with Array.from or with the Spread (...) operator settles the matter.

Good to know

The older getElementsByClassName and getElementsByTagName hand back a live collection instead, one that updates on its own. Removing elements while walking it produces confusing skips.


Searching inside a subtree

Called on an element, the method only looks at its descendants. That is the clean way to work on one component without accidentally grabbing a namesake elsewhere in the page.

JAVASCRIPT
const card = document.querySelector(".product-card");
const price = card.querySelector(".price");

console.log(price.textContent);


The null that breaks everything

A selector that finds nothing throws no error at all. The error shows up one line later, as a TypeError announcing that a property is being read on null.

JAVASCRIPT
const element = document.querySelector(".absent");

console.log(element?.textContent);   // undefined, no error
if (element) element.classList.add("active");

Optional chaining (?.) is enough for a plain read. As soon as a run of statements depends on the element, an explicit condition reads better.


Frequently asked questions

Question

What happens when the selector is invalid?

The method throws a syntax error straight away, which tells a typo in the CSS syntax apart from a search with no result. A badly written selector stops dead, a correct selector that finds nothing simply hands back null.


Question

Is it slow on a large page?

Not at the scale of an everyday interface: engines optimize these searches heavily. The real cost appears when the same search runs again inside a loop or on every mouse move. Store the result in a const and reuse it.


Question

Is getElementById a better choice?

To reach an element by its identifier, getElementById() stays marginally faster and states the intent more plainly. For everything else the flexibility of a CSS selector wins by a wide margin, and code that sticks to a single search method reads more easily.

Related terms

Discover our javaScript glossary

Every word of JavaScript explained simply: keywords, built-in objects, methods, errors and concepts. Clear definitions and examples that actually run, to learn and to troubleshoot.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.