Command Palette

Search for a command to run...

Hectal
PHASE 8Intermediate ~25 min· topic 2 of 8

Topic 8.2

Selecting Elements

In one line

Before you can change an element you must find it: getElementById finds one by id, querySelector and querySelectorAll find elements with any CSS selector, and closest searches upwards; knowing which return live collections and which return null prevents most DOM bugs.

Think of it like this

Finding a friend in a stadium. You can call out their seat number ("Block C, seat 12": exact and fast, like an id), you can say "everyone wearing a red cap, stand up" (a description that may match many people, like a CSS selector), or, if you're holding their lost cap, you can ask "which row does this cap belong to?" and look upwards (like closest). Each way of searching gives you either one person, a group, or nobody.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Selector
A CSS pattern that describes which elements you want, like #cart, .item or ul > li.
null
The value meaning "no object here". DOM lookups return it when nothing matches.
NodeList
A list of nodes, returned by querySelectorAll (a static snapshot) and childNodes (live). It has forEach.
HTMLCollection
A live list of elements from getElementsBy... and children. It has no forEach.
Live list
A list that updates by itself when the page changes.
Static list
A snapshot: it keeps the elements found at the moment you asked, even if the page changes later.
closest
A method that searches from an element upwards through its parents for the first match.
matches
A method that says true or false: does this element fit this selector?

Step by step

01Find one element by id

Give the element an id in HTML and look it up without the #. Always ask yourself: what if it isn't there? Then the result is null.

app.jswhole filejavascript
const cart = document.getElementById("cart");
if (cart) {
  cart.textContent = "3 items";
}

02Find with any CSS selector

querySelector reuses everything you know from CSS. It returns the first match. Note the # and . are needed here, because this is a selector, not a bare id.

app.jswhole filejavascript
const firstPrice = document.querySelector(".product .price");
const activeTab = document.querySelector('[role="tab"][aria-selected="true"]');
const lastItem = document.querySelector("#list li:last-child");

03Find many and loop over them

querySelectorAll gives a NodeList. Loop with for...of or forEach. If you need array methods, convert first.

app.jswhole filejavascript
const prices = document.querySelectorAll(".price");
for (const p of prices) {
  p.classList.add("highlight");
}
const total = Array.from(prices)
  .map((p) => Number(p.textContent))
  .reduce((sum, n) => sum + n, 0);

04Search inside one part of the page

Call the method on an element to look only inside it. This keeps your code from touching similar elements in other parts of the page.

app.jswhole filejavascript
const sidebar = document.querySelector("aside");
const sidebarLinks = sidebar.querySelectorAll("a");

05Search upwards with closest

From a small element deep inside, closest climbs to the nearest ancestor that matches. It starts with the element itself.

Search upwards with closestdiagram
Rendering diagram…

06Pick the right tool

One element with an id: getElementById. One element by any selector: querySelector. Many elements: querySelectorAll (static, has forEach). From an element upwards: closest. You will rarely need getElementsByClassName today, except when you really want a live list.

Try it yourself

  1. 1

    Predict before you run

    In the first example, add console.log(document.querySelector("li:last-child")); and console.log(document.querySelectorAll("a").length);. Write down what you expect, then run.

  2. 2

    Try a bad selector

    Add document.querySelector("#1st"); at the end of the first example. Read the red error. Then try document.getElementById("1st"): why doesn't it throw?

  3. 3

    Make the buggy loop work

    In the third example, keep getElementsByClassName but loop backwards (for (let i = fresh.length - 1; i >= 0; i--)). Predict the first log line now. Why does going backwards avoid the skip?

Code & diagrams

getElementById, querySelector and closest New tab

The console shows elements as `<tag#id.class>`. `querySelector` returns only the first match. `closest("li")` climbed one level from the link; `closest("table")` found nothing, so `null`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

<ul#menu>
<li.item>
<li.item.active>
3
null
<li.item>
<ul#menu>
null
true
Live HTMLCollection versus static NodeList New tab

After adding "C", the live collection and `children` both count 3, but the snapshot from `querySelectorAll` still has 2. `Array.from` turns any list into a real array with `map`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

2 2
2 3 3
true true
function undefined
[ 'A', 'B', 'C' ]
The live-list bug, and the fix New tab

Each removal shrank the live list, so the items shifted down while `i` moved up: paragraphs 2 and 4 were skipped. Looping over a static `querySelectorAll` snapshot removes every one.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Still new after the buggy loop: 2
Still new after the fix: 0

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget that a lookup can fail

Run document.getElementById("cartt").textContent = "3 items"; (a typo in the id).

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Cannot set properties of null (setting 'textContent')

Break #2

Call forEach on an HTMLCollection

Write document.getElementsByClassName("item").forEach((el) => el.remove());.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: document.getElementsByClassName(...).forEach is not a function

Break #3

Pass an id that isn't a valid selector

Call document.querySelector("#1st") for <div id="1st">.

terminal
$ Console
── what you'll see ──
Uncaught SyntaxError: Failed to execute 'querySelector' on 'Document': '#1st' is not a valid selector.

Myth vs fact

Myth

querySelectorAll returns an array.

Fact

It returns a NodeList. It has forEach and works with for...of, but has no map, filter or reduce. Use Array.from() or spread to get an array.

Myth

querySelector is too slow to use; always use getElementById.

Fact

getElementById is a little faster, but both take microseconds. Pick whichever reads better. Slow pages come from layout work (Topic 8.8), not from lookups.

Myth

If nothing matches, querySelectorAll returns null.

Fact

It returns an empty NodeList with length 0, so looping over it safely does nothing. Only the single-element methods (getElementById, querySelector, closest) return null.

Myth

An element with an id is automatically a global variable, so I don't need to select it.

Fact

Browsers do expose ids as properties of window ("named access"), but it's fragile: a variable or another element with the same name wins, and tools and readers can't see where the name comes from. Always select explicitly.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Browsers match selectors right to left: for nav ul li a, they look at each a and walk up its ancestors checking for li, ul, nav. So a selector that starts broad on the right (div *) is slower than one that ends in something specific (.menu-link). For querySelector, simple ids and classes hit fast paths and cached indexes.

  • ▸

    Live HTMLCollections are cached and invalidated by the engine when the DOM changes. Reading collection.length inside a loop that also changes the DOM can make the engine recount repeatedly. Copy to an array first if you're changing the DOM in the loop.

  • ▸

    querySelectorAll matches elements only, never text nodes. To find text, walk the tree with document.createTreeWalker(root, NodeFilter.SHOW_TEXT), which is also how search-and-highlight features are built.

  • ▸

    Elements inside a shadow root (web components, many video players and form widgets) are hidden from document.querySelector. You must first reach the host's shadowRoot (if it is open) and query inside it. This encapsulation is deliberate.

Remember this

  1. 1

    document.getElementById("cart") finds the one element with id="cart". You pass the id without a #. It is the fastest lookup, because the browser keeps a table of ids. Ids should be unique on a page; if two elements share one, you get the first. If nothing matches, you get **null**, the value that means "no object". Reading a property of null is the most common DOM error: Cannot read properties of null.

  2. 2

    querySelector(selector) takes any CSS selector (see the /css course, Phase 1): "#cart", ".item.active", "nav a[href^='/shop']", "li:first-child". It returns the first matching element in document order, or null. querySelectorAll(selector) returns all matches as a NodeList, which is empty (length 0) when nothing matches; it is never null. An invalid selector, such as "#1st" (ids in CSS can't start with a digit), throws a SyntaxError.

  3. 3

    You can call querySelector, querySelectorAll and getElementsBy... on any element, not only on document. Then the search only looks inside that element: menu.querySelectorAll("a") finds links in the menu only. This is faster on big pages and avoids grabbing similar elements elsewhere. One subtle rule: the selector is still matched against the whole document, so menu.querySelector("nav a") can match a link inside menu even if the nav is outside it. Start the selector with :scope (menu.querySelector(":scope > li")) to mean "this element itself".

  4. 4

    Lists of elements come in two kinds. A NodeList (from querySelectorAll, and from childNodes) has length, index access list[0], forEach and works with for...of. An HTMLCollection (from getElementsByClassName, getElementsByTagName and element.children) has length, index access and for...of, but **no forEach**. Neither is a real array: no map or filter. Turn either into an array with Array.from(list) or [...list] (ES2015).

  5. 5

    The bigger difference is live versus static. An HTMLCollection is live: it always reflects the current DOM, so it grows and shrinks as elements are added, removed or change class. The NodeList from querySelectorAll is static: a snapshot taken at the moment you called it. (childNodes is the exception: a live NodeList.) Live lists cause a classic bug: looping over getElementsByClassName("new") while removing the new class makes items disappear from the list mid-loop, so some are skipped.

  6. 6

    element.closest(selector) goes the other way: it checks the element itself, then its parent, then its grandparent, up to <html>, and returns the first one that matches, or null. It is perfect for "I have this button, which card is it in?" element.matches(selector) answers yes or no: does this element match the selector? Both are used constantly with events in Phase 9.

Explain it without notes

01

Compare getElementById, querySelector and querySelectorAll: arguments, return values and what happens when nothing matches.

02

What is the difference between a NodeList and an HTMLCollection, and between live and static lists?

03

What does element.closest(selector) do, and when would you use it?

04

Why can removing classes inside a loop over getElementsByClassName skip elements? How do you avoid it?

Practice

01

Given a list of products where some have class sale, log how many are on sale and the text of the first one, using one selector per lookup.

02

Given <button class="remove"> inside <li class="task" data-id="7">, write a function taskIdOf(button) that returns the task's data-id attribute using closest.

03

Turn all .price elements into an array of numbers and log their total.

Trade-offs

  • ↔

    Ids are fast and clear but must be unique, which makes reusable components harder; classes and data-* attributes (Topic 8.4) work for repeated parts, and scoping a search to a container keeps it predictable.

  • ↔

    Live collections save you from re-querying but surprise you while the DOM changes; static snapshots are predictable but go stale, so query again after big changes.

Done when you can

  • I can find elements with getElementById, querySelector and querySelectorAll.

  • I check for null when an element might not exist.

  • I can explain NodeList versus HTMLCollection and live versus static.

  • I can convert a list to an array with Array.from or spread.

  • I can use closest and matches to search upwards and test elements.

  • I can search inside one element instead of the whole document.