Learn by reading through in order

The DOM Tree and Selecting Elements — querySelector and querySelectorAll

Covers querySelector and querySelectorAll for getting elements from the DOM tree: searching by class, id, and data attribute, turning a NodeList into an array, and ?. for missing elements.

Say you want to read the prices of every product in a product list. Selecting by id, like #total, only works for elements that have an id, so it doesn't suit a list of products that all share the same structure.

This article covers selectors for finding elements by conditions other than an id, querySelectorAll for collecting every match, and how to read an element that might not be there.

Getting a Single Element — querySelector and Selectors

Now suppose you want only the heading of the important notice in a list of notices. The list is made up of identical li elements, none of which has an id. An id is a name that only one element on a page can have, so it can't serve as a marker for a kind of item, such as "important notices".

A selector (a string describing the element you're looking for) specifies elements by tag name, as in li, by id, as in #news, or by class (a name you give to every element of the same kind), as in .notice. document.querySelector(selector) returns the first element in the DOM tree that matches.

// Set up a list of notices (an element can have several classes, separated by spaces)
document.body.innerHTML = `<ul id="news">
  <li class="notice">Shipping fee update</li>
  <li class="notice important">Unscheduled maintenance</li>
  <li class="notice">New fall products</li>
</ul>`;

// Find by tag name / by class (the class name goes after the .)
console.log(document.querySelector("li").textContent);           // Shipping fee update
console.log(document.querySelector(".notice").textContent);      // Shipping fee update
console.log(document.querySelector(".important").textContent);   // Unscheduled maintenance
Which Element Each Selector Finds
body element — searched from the top down
ul element — id is news
1st li — class is notice
  • "li" and ".notice" — both stop searching here
  • Text is "Shipping fee update"
2nd li — classes are notice and important
  • ".important" — stops searching here
  • Text is "Unscheduled maintenance"
3rd li — class is notice
  • Text is "New fall products"
  • None of the three selectors gets this far
Three li elements have notice, but ".notice" stops searching at the first one. What you get back is the first element that matches.

The second li has two classes, so it matches both .notice and .important. To get only the second one, write a condition that nothing but the second one matches, such as .important.

On a product list page, get elements one at a time, choosing between tag names, classes, and ids. The HTML is already set up by the assignment to document.body.innerHTML at the top.

① Find the first product by tag name and print its text.

② Find the product by the class that only the recommended product has, and print its text.

③ Find the element that shows the product count by its id, and print its text.

(If it runs correctly, an explanation will appear.)

JavaScript / TypeScript Editor

Run code to see output

Going Through a Whole List — querySelectorAll and NodeList

What if you want the headings of all the notices at once? querySelector stops searching at the first match, so however many times you call it with the same selector, you only get "Shipping fee update" and never reach the second one or beyond.

document.querySelectorAll(selector) returns every matching element in a NodeList (an array-like object that holds the elements it found in DOM tree order). You can read how many there are with length and handle them one at a time with forEach.

document.body.innerHTML = `<ul id="news">
  <li class="notice">Shipping fee update</li>
  <li class="notice important">Unscheduled maintenance</li>
  <li class="notice">New fall products</li>
</ul>`;

// Get every matching element as a NodeList
const notices = document.querySelectorAll(".notice");
console.log(notices.length);   // 3
notices.forEach((notice) => console.log(notice.textContent));
// Shipping fee update
// Unscheduled maintenance
// New fall products

// notices.map(...) is a TypeError. Turn it into an array before calling map
const titles = [...notices].map((notice) => notice.textContent);
console.log(titles.join(" / "));   // Shipping fee update / Unscheduled maintenance / New fall products
Turn It into an Array Before Calling map
notices.map(...)A NodeListhas no mapThrows aTypeErrortitles isnever created[...notices].map(...)An array of thesame 3 elementsThe array's mapruns on each one["Shipping feeupdate", …]3 strings
Calling map on the NodeList itself throws an error, while turning it into an array first makes it work. Call map and filter after turning the NodeList into an array.

Even as a NodeList, you can read a single element by its index, as in notices[1]. The array made with spread syntax is a separate object, so notices stays a NodeList after the conversion, and you can keep using it with forEach.

On an order receipt page, read all the purchased items at once, then check how many there are and how many cost 2,000 yen or more. The receipt HTML is already set up by the assignment to document.body.innerHTML at the top.

① Get all the item elements on the receipt and print how many there are.

② Go through the elements you got one at a time and print each item's text in order.

③ Get all the price elements, turn them into an array, keep only the ones that are 2,000 yen or more, and print how many there are.

JavaScript / TypeScript Editor

Run code to see output

Pointing to One Item by Its Number — data Attributes and Attribute Selectors

Suppose you need just the notice numbered N-103. If the same notices also appear in another section of the page, their ids would be duplicated, so an id won't work. A class is a name for grouping elements of the same kind, so it isn't a good place to store a number either.

A data attribute (an attribute whose name starts with data-) can hold any custom value you like, such as a number. Selectors that search by attribute go in square brackets: [data-id] matches elements that have the attribute, and [data-id="N-103"] matches elements whose value matches as well.

document.body.innerHTML = `<ul id="news">
  <li class="notice" data-id="N-101">Shipping fee update</li>
  <li class="notice important" data-id="N-102">Unscheduled maintenance</li>
  <li class="notice" data-id="N-103">New fall products</li>
</ul>`;

// Search only by whether the attribute exists (all 3 match)
console.log(document.querySelector("[data-id]").textContent);          // Shipping fee update

// Search for an element whose value matches too (the value is in ", so wrap the whole selector in ')
console.log(document.querySelector('[data-id="N-103"]').textContent);  // New fall products
From an Attribute to a Selector String
data-id="N-103"[data-id="N-103"]'[data-id="N-103"]'The li's attributename="value"Square bracketsmake it a selectorIt contains ", sowrap it in '
Wrapping an HTML attribute in square brackets makes a selector, and you wrap that in ' to pass it to JavaScript. The " around the value and the outer ' do different jobs.

[data-id] without a value matches all three and returns the first, while [data-id="N-103"], with the value, points only to the one whose number matches. The table below lists the selectors used in this article and the element each one returns from the notice list.

SelectorMatchesReturned from the notices
liTag name is li1st li (Shipping fee update)
.importantclass includes important2nd li (Unscheduled maintenance)
#newsid is newsThe ul element
[data-id]Has a data-id attribute1st li (Shipping fee update)
[data-id="N-103"]data-id is N-1033rd li (New fall products)

Values Starting with a Digit Fail Without Quotes

Some values work without quotes, like [data-id=N-103], but a value that starts with a digit, like [data-id=102], can't be read as a selector, and the line throws an "is not a valid selector" error. Always wrap values in ".

In a stationery store's inventory list, pick out the items you want using their product numbers and sale markers. The inventory HTML is already set up by the assignment to document.body.innerHTML at the top.

① Find the element with product number P-203 and print its text.

② Find the element that has the sale attribute and print its text.

③ Get every element that has the product number attribute and print how many there are.

JavaScript / TypeScript Editor

Run code to see output

Keeping Going When an Element Is Missing — null and ?.

Think about a home page that shows a heading only when there's an urgent notice. On days without one, the element doesn't exist, so the line that reads the heading's text throws a TypeError, and nothing after it runs.

When no element matches, querySelector returns null. If you add optional chaining, ?., to the result before reading .textContent, it returns undefined without reading any further when the value is null, and adding ?? after it lets you set a default.

// A list with no urgent notice (class is urgent) yet
document.body.innerHTML = `<ul><li class="notice">New fall products</li></ul>`;
const urgent = document.querySelector(".urgent");
console.log(urgent);                          // null
// urgent.textContent would throw a TypeError
console.log(urgent?.textContent);             // undefined
console.log(urgent?.textContent ?? "None");   // None

// In a list that has an urgent notice, compare with and without the .
document.body.innerHTML = `<ul><li class="urgent">Server outage</li></ul>`;
console.log(document.querySelector(".urgent")?.textContent ?? "None");   // Server outage
console.log(document.querySelector("urgent")?.textContent ?? "None");    // None
Three Searches and the Text They Show
querySelector(…)?.textContent?? "None"No urgent notice,search ".urgent"Has urgent notice,search ".urgent"Has urgent notice,search "urgent"Gets null;?. stops thereThe li is found;its text is readLooks for tag nameurgent; gets nullShows "None"Shows "Serveroutage"It's there, butshows "None"
Columns 1 and 3 both go from null to "None". ?. can't tell a missing element from a typo in the selector.

Only use ?. on elements that may be missing on some days, like the urgent notice. If you add it to elements that are always there, you won't notice typos, so leave it off, let the code throw a TypeError, and use the line number to track down the cause.

A NodeList with 0 Elements Isn't null

Even when nothing matches, querySelectorAll doesn't return null; it returns a NodeList whose length is 0. Calling forEach on it doesn't throw an error and simply does nothing, so to find out that nothing matched, check length === 0 rather than using ?..

On a product detail page, read the reviews and the sale price, either of which may be missing. The product detail HTML is already set up by the assignment to document.body.innerHTML at the top.

① Print the product name's text.

② Get all the review li elements, and if there are none, print "No reviews yet".

③ Print the sale price's text without throwing an error if the element is missing.

④ If there's no sale price, print the regular price's text instead.

JavaScript / TypeScript Editor

Run code to see output
QUIZ

Knowledge Check

Answer each question one by one.

Q1With 3 li elements that have data-id, what does querySelector("[data-id]") return?

Q2What happens if you call map directly on the return value of querySelectorAll(".notice")?

Q3When the element doesn't exist, what is the value of querySelector(".sale-price")?.textContent?