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.
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")); // nullThe 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.
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.
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.
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.
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
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.
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.
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.