blog / guides

selecting DOM elements: 10 examples you can try

by Luke published 8 min read

the short answer

querySelector and querySelectorAll accept any CSS selector, so getting better at selecting elements mostly means knowing more selectors. Attribute selectors find inputs by type or name, :nth-child picks by position, :not leaves some out and :checked finds ticked boxes. closest walks up from an element to its container, matches tests an element against a selector, and Array.from turns a NodeList into a real array.

querySelector and querySelectorAll accept any CSS selector, so the more selectors you know, the more precisely you can find things on a page. This page is a set of ten examples, all run against the same small page, that go beyond selecting by tag, class and id.

If the basics are new to you, start with our guide to selecting DOM elements in JavaScript, which explains what the DOM is, the difference between the two methods, and what happens when nothing matches.

The page used in every example

A book club page with a list of books, a sign-up form and a card:

<main>
  <h1>Book club</h1>
  <ul id="books">
    <li class="book" data-pages="320">Moon Garden</li>
    <li class="book read" data-pages="208">River Song</li>
    <li class="book" data-pages="464">Paper Kites</li>
    <li class="book read" data-pages="176">Night Train</li>
  </ul>
  <form id="join">
    <input type="text" name="name" placeholder="Your name">
    <input type="email" name="email" placeholder="Email address">
    <label><input type="checkbox" name="news" checked> Send me news</label>
    <button type="submit">Join</button>
  </form>
  <article class="card">
    <h2>Next meeting</h2>
    <p>Thursday at 7pm</p>
    <button class="remind">Remind me</button>
  </article>
</main>

The run boxes on this blog have no web page to work with, so these examples are shown rather than run. Each one was tested on this exact page in a real browser, and the output under it is what the console printed. The end of this guide shows how to try them yourself.

1. By attribute

const email = document.querySelector('input[type="email"]');
console.log(email.placeholder);
Email address

Square brackets match an attribute. input[type="email"] means an input whose type is email. Leave out the value, as in [placeholder], to match any element that has the attribute at all.

2. By name

const news = document.querySelector('[name="news"]');
console.log(news.checked);
true

Form fields are usually picked by name, because the name is what the form sends. checked tells you whether a checkbox is ticked.

3. Only the ticked boxes

const ticked = document.querySelectorAll("input:checked");
console.log(ticked.length);
1

:checked matches ticked checkboxes and chosen radio buttons. It reflects what the visitor has done right now, so running this after they untick the box gives 0.

4. By position

console.log(document.querySelector("#books li:nth-child(2)").textContent);
console.log(document.querySelector("#books li:last-child").textContent);
River Song
Night Train

:nth-child(2) is the second child of its parent, counting from 1. :first-child and :last-child save you working out the number. :nth-child(odd) and :nth-child(even) match every other one, which is handy for striped lists.

5. Everything except some

const unread = document.querySelectorAll(".book:not(.read)");
unread.forEach((book) => console.log(book.textContent));
Moon Garden
Paper Kites

:not() leaves out anything that matches the selector inside it. Here it means books without the read class.

6. Children, not all descendants

console.log(document.querySelectorAll("#join input").length);
console.log(document.querySelectorAll("#join > input").length);
3
2

A space means anywhere inside, so the first line also counts the checkbox tucked inside its label. > means directly inside, so the checkbox is left out.

7. Reading data attributes

const first = document.querySelector(".book");
console.log(first.dataset.pages);
console.log(Number(first.dataset.pages) + 1);
320
321

Attributes that start with data- are yours to invent, and dataset reads them. The value always comes back as text, so Number() turns it into a number before any maths.

8. NodeList to array

const books = Array.from(document.querySelectorAll(".book"));
const titles = books.map((book) => book.textContent);
console.log(titles.join(", "));
const long = books.filter((book) => Number(book.dataset.pages) > 300);
console.log(long.length);
Moon Garden, River Song, Paper Kites, Night Train
2

querySelectorAll returns a NodeList. It has forEach, but not map, filter or reduce. Array.from copies it into a real array so every array method works.

9. Walking up with closest

const button = document.querySelector(".remind");
const card = button.closest(".card");
console.log(card.querySelector("h2").textContent);
Next meeting

closest starts at the element and climbs through its parents until something matches. It is the standard way to get from a button that was clicked to the card, row or form it belongs to, without caring how deeply it is nested.

10. Testing with matches

document.querySelectorAll(".book").forEach((book) => {
  console.log(book.textContent, book.matches(".read"));
});
Moon Garden false
River Song true
Paper Kites false
Night Train true

matches asks whether an element fits a selector, and answers true or false. It is useful inside click handlers, where you often need to know what kind of element was clicked. One listener on the whole list can test each clicked element with matches(".book") and handle every book, including any that are added to the list later.

Choosing a good selector

Most of the time, several selectors find the same element. #books li:nth-child(2) and .read both reach River Song today. The question is which one will still work after someone edits the page.

  • Prefer names that describe the thing. A class like book or a name like email says what the element is, so it survives a redesign.
  • Avoid long chains. main > ul li:nth-child(3) breaks the moment a new list or book is added above it.
  • Use position only when position is the point. Picking the first row of a table is fine. Picking a book because it happens to be third is fragile.
  • Keep JavaScript hooks separate from styling. Some teams add a data attribute just for scripts, such as data-js="remind", so changing a class for the look never breaks the code.

A good test: if you can read the selector aloud and it describes what you want, it is probably a sturdy one.

Common mistakes

Expecting nth-child to count by class

console.log(document.querySelector(".read:nth-child(1)"));
null

This looks for an element that is the first child and also has the read class. The first child, Moon Garden, has not been read, so nothing matches. :nth-child always counts every child. To get the first read book, select .read on its own with querySelector.

Missing quotes inside the brackets

input[type=email] happens to work, but values with spaces or symbols need quotes, such as [data-title="Paper Kites"]. Using quotes every time avoids the problem.

Reading data attributes with dashes

An attribute called data-page-count is read as dataset.pageCount. Each dash and the letter after it become a capital letter.

Expecting the list to update

The NodeList from querySelectorAll is a snapshot. If you add a fifth book afterwards, the list you already have still holds four. Select again after changing the page.

Try them yourself

Save the page above in a file called books.html, between a <body> and </body> inside a basic HTML skeleton. Open it in Chrome, Edge or Firefox, then open the console with Ctrl, Shift and J in Chrome or Edge on Windows, or Cmd, Option and J in Chrome on a Mac. Paste in any example and press Enter. A refresh puts the page back as it was.

A short exercise

Log the total number of pages in the books that have not been read yet. Your output should be 784.

Hint: combine :not() from example 5, Array.from from example 8 and Number() from example 7, then add up with reduce or a loop.

One possible answer

const unread = Array.from(document.querySelectorAll(".book:not(.read)"));
const pages = unread.reduce((total, book) => total + Number(book.dataset.pages), 0);
console.log(pages);
784

Moon Garden has 320 pages and Paper Kites 464, which makes 784. Without Number(), the plus signs would join text and give 0320464.

Your next step

Selecting is the first step of everything interactive on the web. Gwecode’s JavaScript Level 2 puts a live page next to your code, then moves from selecting to changing words, switching attributes on and off and reacting to clicks. Its level check is a working shopping list app. Every lesson is free and runs in your browser. Create a free account to keep your progress, and try the lesson below.

practise this · javascript · lessonMany at oncestart the lesson

questions

How do I select an input by its type or name?

Pass querySelector an attribute selector in square brackets, such as input[type="email"] for an email box or [name="email"] for the field named email. Put quotes around the value inside the brackets.

What is the difference between nth-child and nth-of-type?

nth-child counts every child of the parent, whatever its tag or class. nth-of-type counts only children with the same tag. Neither one counts by class, which is a common surprise.

How do I find the parent card of a clicked button?

Call closest with a selector on the button, such as button.closest(".card"). It checks the button itself and then each parent in turn, and returns the first match, or null if there is none.

How do I use map or filter on a NodeList?

Turn it into an array first with Array.from(list). A NodeList has forEach, but not map, filter or reduce.

How do I read a data attribute?

Use dataset with the part after data-, so data-pages becomes element.dataset.pages. The value is always text, so wrap it in Number() before doing maths with it.

keep going.