Selecting elements in the DOM

Understand what the DOM is, find elements with querySelector, querySelectorAll and getElementById, and start building a quiz app.

  • Beginner
  • 9 min read
  • Updated 17 Oct 2026

What you will learn

  • Select elements with CSS selectors
  • Loop over a NodeList
  • Explain what the DOM is and how it relates to your HTML
  • Handle a selector that matches nothing

Before you start

To change a page with JavaScript, you first have to find the part you want to change. This lesson explains the DOM, the browser's live model of your page, and shows how to select elements from it. You will also set up the quiz app that you build over the next five lessons.

What the DOM is

When a browser loads HTML, it does not keep the text of your file. It parses it into a tree of objects called the Document Object Model, or DOM. Every element becomes an object, called a node, with properties and methods you can use from JavaScript.

Take this HTML:

HTML
<main>
  <h1>Quiz</h1>
  <p>Answer three questions.</p>
</main>

The browser builds a tree like this:

Plain text
main
├── h1
│   └── "Quiz"
└── p
    └── "Answer three questions."

The main element is the parent of h1 and p, which are its children and each other's siblings. The text inside each element is a node too, called a text node.

Three things are worth knowing about the DOM:

  • It is live. When JavaScript changes the DOM, the browser updates the screen to match. That is how every interactive page works.
  • It is not your HTML file. The browser fixes mistakes as it parses, and scripts can change the tree afterwards. The Elements panel in DevTools shows the current DOM, not your source.
  • It is provided by the browser. The DOM is a Web API, not part of the JavaScript language. That is why document does not exist in Node.js.

The whole tree is reachable through the global document object. Try these in the console on any page:

JavaScript
document.title
// → the page title
document.body
// → the body element
document.documentElement
// → the html element

Setting up the quiz app

Over lessons 14 to 18 you will build a small quiz. Create a folder called quiz with two files. First, quiz/index.html:

HTML
<!doctype html>
<html lang="en-GB">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>JavaScript quiz</title>
  <script src="quiz.js" defer></script>
</head>
<body>
  <main>
    <h1>JavaScript quiz</h1>
    <section id="quiz">
      <p id="progress">Question 1 of 3</p>
      <h2 id="question-text">Question text goes here</h2>
      <ul class="options">
        <li><button type="button" class="option">Option A</button></li>
        <li><button type="button" class="option">Option B</button></li>
        <li><button type="button" class="option">Option C</button></li>
      </ul>
      <p id="status" role="status" aria-live="polite"></p>
      <button type="button" id="next-button">Next question</button>
    </section>
  </main>
</body>
</html>

The options are real button elements, so they work with a keyboard and screen readers without extra code. The status paragraph is an ARIA live region: when JavaScript later puts "Correct!" in it, screen readers announce the change without moving focus. It starts empty on purpose, because live regions announce changes, and must exist in the page before the change happens. The Web Accessibility Essentials course covers live regions in depth.

Then quiz/quiz.js, with the questions held in an array of objects, as in lesson 13:

JavaScript
const questions = [
  {
    id: 1,
    text: "Which keyword declares a variable that cannot be reassigned?",
    options: ["let", "const", "var"],
    answer: "const",
  },
  {
    id: 2,
    text: "What does typeof null return?",
    options: ["\"null\"", "\"object\"", "\"undefined\""],
    answer: "\"object\"",
  },
  {
    id: 3,
    text: "Which loop is best for the values of an array?",
    options: ["for...in", "for...of", "do...while"],
    answer: "for...of",
  },
];

The script is loaded with defer, so it runs after the HTML has been parsed and every element exists. You will add the selection code below to this file.

querySelector: the first match

document.querySelector() takes any CSS selector and returns the first element that matches:

JavaScript
const questionText = document.querySelector("#question-text");
const firstOption = document.querySelector(".option");
const heading = document.querySelector("h1");

console.log(questionText.textContent);
// Question text goes here
console.log(firstOption.textContent);
// Option A

Because it accepts any selector you would write in CSS, it can be as precise as you need:

JavaScript
const nextButton = document.querySelector("#quiz > button");
const secondOption = document.querySelector(".options li:nth-child(2) button");

Keep selectors simple and tied to meaning. A selector that depends on the exact nesting of the page breaks when someone moves an element. An id or a class chosen for the purpose is sturdier.

When nothing matches

If no element matches, querySelector() returns null:

JavaScript
const missing = document.querySelector("#does-not-exist");
console.log(missing);
// null

missing.textContent = "Hello";
// Uncaught TypeError: Cannot set properties of null (setting 'textContent')

This error usually means one of three things: a typo in the selector, a forgotten # or ., or the script running before the element exists, which defer prevents. If an element is genuinely optional, check before using it:

JavaScript
const banner = document.querySelector(".promo-banner");

if (banner) {
  banner.hidden = true;
}

An invalid selector, such as "#" on its own, throws a SyntaxError instead of returning null.

getElementById

document.getElementById() finds the element with a given id. Pass the id without a #:

JavaScript
const statusRegion = document.getElementById("status");
const nextButton = document.getElementById("next-button");

It returns the element or null, just like querySelector("#status"). It is slightly faster, though the difference is too small to matter in practice. Use whichever reads better to you, and be consistent; this course mostly uses querySelector so that every selection looks the same.

querySelectorAll: every match

document.querySelectorAll() returns all matching elements, in document order, as a NodeList:

JavaScript
const optionButtons = document.querySelectorAll(".option");

console.log(optionButtons.length);
// 3
console.log(optionButtons[0].textContent);
// Option A

If nothing matches, you get an empty NodeList with a length of 0, never null. So you do not need a null check, but a typo silently gives you nothing to work with.

Looping over a NodeList

A NodeList is not an array, but it is iterable, so for...of works:

JavaScript
for (const button of optionButtons) {
  console.log(button.textContent);
}
// Option A
// Option B
// Option C

It also has a forEach() method:

JavaScript
optionButtons.forEach((button, index) => {
  console.log(`${index}: ${button.textContent}`);
});
// 0: Option A
// 1: Option B
// 2: Option C

It does not have array methods such as map() or filter(). Convert it to an array first when you need them:

JavaScript
const labels = Array.from(optionButtons).map((button) => button.textContent);
console.log(labels);
// ["Option A", "Option B", "Option C"]

You will meet array methods properly in lesson 22.

A static snapshot

The NodeList from querySelectorAll() is static: it is a snapshot of the matches at the moment you called it. If you add another .option button to the page later, the existing NodeList does not include it. Call querySelectorAll() again when you need an up-to-date list.

Older methods such as getElementsByClassName() and getElementsByTagName() return a live HTMLCollection that updates automatically. That sounds convenient, but it causes surprising bugs when you change the page while looping. Prefer querySelectorAll().

Searching inside an element

querySelector() and querySelectorAll() are also methods of every element. Called on an element, they only search inside it:

JavaScript
const quiz = document.querySelector("#quiz");
const quizButtons = quiz.querySelectorAll("button");

console.log(quizButtons.length);
// 4

That finds the three options and the Next button, but no buttons elsewhere on the page. Scoping searches like this makes code faster to reason about and safer when a page contains several similar components.

To go upwards instead, closest() finds the nearest ancestor, including the element itself, that matches a selector:

JavaScript
const firstOption = document.querySelector(".option");
console.log(firstOption.closest("section").id);
// quiz

You will use closest() with events in lesson 29.

Putting it together

Add this below the questions array in quiz/quiz.js:

JavaScript
const quiz = document.querySelector("#quiz");
const progress = quiz.querySelector("#progress");
const questionText = quiz.querySelector("#question-text");
const optionButtons = quiz.querySelectorAll(".option");
const statusRegion = quiz.querySelector("#status");
const nextButton = quiz.querySelector("#next-button");

console.log(`Found ${optionButtons.length} option buttons`);
console.log(`First question: ${questions[0].text}`);

Open quiz/index.html in a browser and look at the console:

Plain text
Found 3 option buttons
First question: Which keyword declares a variable that cannot be reassigned?

Selecting every element you need once, at the top of the file, and storing them in well-named const variables keeps the rest of your code readable. In the next lesson you will use these variables to show the first question and its options on the page.

In the Elements panel of DevTools, select an element and type $0 in the console. It refers to the element you selected, which is handy for testing selectors and properties.

Summary

  • The DOM is the browser's live tree of objects built from your HTML, reached through document.
  • querySelector() returns the first element matching a CSS selector, or null if none does.
  • getElementById() finds an element by id, without the #.
  • querySelectorAll() returns a static NodeList of every match, which may be empty but is never null.
  • Loop over a NodeList with for...of or forEach(); use Array.from() to get array methods.
  • Call querySelector() on an element to search only inside it, and closest() to search upwards.

Further reading