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,.itemorul > 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) andchildNodes(live). It hasforEach. - HTMLCollection
- A live list of elements from
getElementsBy...andchildren. It has noforEach. - 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
trueorfalse: 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.
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.
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.
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.
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.
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
Predict before you run
In the first example, add
console.log(document.querySelector("li:last-child"));andconsole.log(document.querySelectorAll("a").length);. Write down what you expect, then run. - 2
Try a bad selector
Add
document.querySelector("#1st");at the end of the first example. Read the red error. Then trydocument.getElementById("1st"): why doesn't it throw? - 3
Make the buggy loop work
In the third example, keep
getElementsByClassNamebut 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
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
Console
Nothing logged yet.Expected console output
<ul#menu>
<li.item>
<li.item.active>
3
null
<li.item>
<ul#menu>
null
trueAfter 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
Console
Nothing logged yet.Expected console output
2 2
2 3 3
true true
function undefined
[ 'A', 'B', 'C' ]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
Console
Nothing logged yet.Expected console output
Still new after the buggy loop: 2
Still new after the fix: 0Break 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).
Break #2
Call forEach on an HTMLCollection
Write document.getElementsByClassName("item").forEach((el) => el.remove());.
Break #3
Pass an id that isn't a valid selector
Call document.querySelector("#1st") for <div id="1st">.
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 eachaand walk up its ancestors checking forli,ul,nav. So a selector that starts broad on the right (div *) is slower than one that ends in something specific (.menu-link). ForquerySelector, 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.lengthinside 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. - ▸
querySelectorAllmatches elements only, never text nodes. To find text, walk the tree withdocument.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'sshadowRoot(if it is open) and query inside it. This encapsulation is deliberate.
Remember this
- 1
document.getElementById("cart")finds the one element withid="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 ofnullis the most common DOM error:Cannot read properties of null. - 2
querySelector(selector)takes any CSS selector (see the/csscourse, Phase 1):"#cart",".item.active","nav a[href^='/shop']","li:first-child". It returns the first matching element in document order, ornull.querySelectorAll(selector)returns all matches as a NodeList, which is empty (length 0) when nothing matches; it is nevernull. An invalid selector, such as"#1st"(ids in CSS can't start with a digit), throws aSyntaxError. - 3
You can call
querySelector,querySelectorAllandgetElementsBy...on any element, not only ondocument. 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, somenu.querySelector("nav a")can match a link insidemenueven if thenavis outside it. Start the selector with:scope(menu.querySelector(":scope > li")) to mean "this element itself". - 4
Lists of elements come in two kinds. A NodeList (from
querySelectorAll, and fromchildNodes) haslength, index accesslist[0],forEachand works withfor...of. An HTMLCollection (fromgetElementsByClassName,getElementsByTagNameandelement.children) haslength, index access andfor...of, but **noforEach**. Neither is a real array: nomaporfilter. Turn either into an array withArray.from(list)or[...list](ES2015). - 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
querySelectorAllis static: a snapshot taken at the moment you called it. (childNodesis the exception: a live NodeList.) Live lists cause a classic bug: looping overgetElementsByClassName("new")while removing thenewclass makes items disappear from the list mid-loop, so some are skipped. - 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, ornull. 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
Compare getElementById, querySelector and querySelectorAll: arguments, return values and what happens when nothing matches.
What is the difference between a NodeList and an HTMLCollection, and between live and static lists?
What does element.closest(selector) do, and when would you use it?
Why can removing classes inside a loop over getElementsByClassName skip elements? How do you avoid it?
Practice
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.
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.
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,querySelectorandquerySelectorAll.I check for
nullwhen 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.fromor spread.I can use
closestandmatchesto search upwards and test elements.I can search inside one element instead of the whole document.