completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Walking & searching the DOM

Learn to find elements by walking parent, child, and sibling relationships, searching with CSS selectors, using closest and matches, and choosing between live and static DOM collections.

By the end, you can
  • 01
    Walk the treeGo to a parent, a child, or a sibling, and know when you might land on blank text instead of a tag.
  • 02
    Search with selectorsFind elements with querySelector, querySelectorAll, matches, and closest.
  • 03
    Pick the right kind of listTell a live list from a snapshot, and remove items in a loop without skipping any.

Find your place in the tree

The browser turns your HTML into a tree of objects. This tree is called the DOM. Each object in the tree is a node. Once your code holds one node, it can reach other nodes in two ways:

  • Walk. Follow a link from the node you have: "go to my parent", "go to my first child", or "go to the next sibling". A sibling is a node with the same parent.
  • Search. Describe what you want with a CSS selector, such as li or .done, and let the browser find it. You can ask for the first match or for every match.

This lesson builds on The browser environment and The DOM tree. Here you move around the tree on purpose. For example, you go from a clicked button up to its card, or from a menu down to its items.

Real-life analogyWalking a family tree

You sit at a family dinner. You can point to your parent, your children, or the brother next to you. You don't need to search the whole room. You start from yourself and follow one family link.

In real life: Point to your parent
In JavaScript: parentElement
In real life: Point to your children
In JavaScript: children, firstElementChild, lastElementChild
In real life: Point to the brother or sister next to you
In JavaScript: nextElementSibling or previousElementSibling
The short definition

DOM navigation means getting from one node to another. You either follow a family link (parentElement, children, siblings) or search with a selector (querySelector, closest, and similar methods).

One difference trips up many people: elements versus nodes.

  • An element is a tag, such as ul or li.
  • A node is anything in the tree. That includes elements, but also text and comments. Even the spaces and line breaks between your tags become text nodes. We call these blank ones whitespace text nodes.

The property name tells you which kind you get. Names with Element in them, and children, give only elements. childNodes and firstChild give every kind of node.

Parents, children & siblings

INTERACTIVE

Family links are the quickest way to move when the node you want is close by. The browser already keeps these links in the tree. So it just follows one link. It does not have to read a selector.

How to read this table: each row is one direction. The middle column skips text and comments. The last column includes them.

Properties for walking up, down, and sideways
Where you want to goElements onlyEvery kind of node
Up to the parentparentElement gives the parent element, or null if the parent is not an element.parentNode gives any parent node. At the very top, that is the document.
Down to the childrenchildren, firstElementChild, and lastElementChild give only tags.childNodes, firstChild, and lastChild also give text and comments.
Sideways to a siblingnextElementSibling and previousElementSibling skip blank text.nextSibling and previousSibling can land on blank text.
When to use themUse these for tags you style or click. That is most code.Use these only when text or comments matter to you.

The top of the tree has a surprise. document.documentElement is the html element. Its parent is the document object. The document is a node, but it is not an element. So:

  • parentNode of html is the document.
  • parentElement of html is null, because there is no parent element.
The document is a node, not an elementPop out in the code editor (opens in a new tab)JavaScript
const html = document.documentElement;
console.log(html.parentElement === null);
console.log(html.parentNode === document);

Line 1 saves the html element in html. Line 2 prints true: it has no parent element. Line 3 prints true: its parent node is the document.

The same thing happens at every dead end. The last child has no next sibling, so nextElementSibling gives null. Check for null before you read a property from the result.

The menu, ul#menu, has three li children: home, docs, and shop. firstElementChild goes down from the menu to li home. nextElementSibling goes right from li home to li docs. parentElement goes up from li docs back to the menu.ul#menuli homeli docsli shopfirstElementChildnextElementSiblingparentElement
The three moves in the step-through below: down to the first child, right to the next sibling, then up to the parent. Each move follows a link the browser already keeps.
Step through one walk
Step 0 of 5Ready
Your turn: follow the blue line

Pick elements only or every kind of node, then step through the same walk.

Running in
  1. script
Next: line 1
let current = list.firstElementChild;current = current.nextElementSibling;current = current.parentElement;console.log(current.id);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Choose which properties the code uses.

Changing this starts the step-through again. With every kind of node, the walk can land on blank text.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Now walk real nodes in your browser. The buttons with Element in their names skip whitespace text nodes. The other buttons can land on one, because the mini page has line breaks and spaces between its tags.

Walk the tree one link at a time
Walk with family linksPop out in the code editor (opens in a new tab)JavaScript
const list = document.querySelector("#menu");let current = list.firstElementChild;current = current.nextElementSibling;current = current.parentElement;current = current.parentNode;current = current.firstChild;current = current.nextSibling;
Mini pageul#menu
Path so farlist
Try it yourself

You start on the menu. Buttons with Element in the name skip blank text. The others can land on it.

Each button reads one property from the highlighted node and moves there. A blank text node has no box of its own, so its parent gets the highlight.

querySelector & querySelectorAll

INTERACTIVE

When the node you want is not close by, search for it with a CSS selector. A selector is the pattern CSS uses to pick elements:

  • li picks every li tag.
  • .done picks every element with the class done.
  • #menu picks the element with the id menu.

There are two search methods:

  • querySelector(selector) gives the first matching element. If nothing matches, it gives null.
  • querySelectorAll(selector) gives every matching element in a list called a NodeList. This list has a forEach method. It is static: it is a snapshot, so it does not change when the page changes later.
Left: querySelector with the selector li gives one element, li home, the first match. Right: querySelectorAll with the selector li gives a NodeList that holds all three: home, docs, and shop.querySelector("li")li homefirst match onlyquerySelectorAll("li")homedocsshopNodeList
Same selector, two answers. querySelector stops at the first match. querySelectorAll collects every match into a list.
Real-life analogyA librarian with one book or every book

You ask a librarian for "the first red JavaScript book". You get one book. You ask for "every red JavaScript book". You get a stack of books.

In real life: Ask for the first matching book
In JavaScript: querySelector(selector)
In real life: Ask for every matching book
In JavaScript: querySelectorAll(selector)
In real life: Ask in words the librarian can't understand
In JavaScript: A broken selector throws a SyntaxError

You can call both methods on document or on one element. On an element, the search only gives back elements inside it. These are its descendants: its children, their children, and so on.

But the selector itself still looks at the whole page. So menu.querySelectorAll("div p") can match a p inside menu even when the div is outside menu.

To mean "the element I called this on", write :scope. For example, :scope > li means li elements whose parent is that element.

Try a selector
Search the menu with selectorsPop out in the code editor (opens in a new tab)JavaScript
const menu = document.querySelector("#menu");const firstDone = menu.querySelector(".done");const allItems = menu.querySelectorAll("li");const directItems = menu.querySelectorAll(":scope > li");const dataItems = menu.querySelectorAll("[data-id]");
Matchessearching the mini page
Try it yourself

3 matches. querySelector gives the first one.

Every match is highlighted. The first match, the one querySelector gives, has a thicker outline. Only a broken selector throws an error.
No match is different from a broken selectorPop out in the code editor (opens in a new tab)JavaScript
const missing = document.querySelector("#missing");
const noItems = document.querySelectorAll("#missing");
console.log(missing === null);
console.log(noItems.length);
try {
  document.querySelector("li[");
} catch (error) {
  console.log(error.name);
}

The page has no element with the id missing.

  • Line 3 prints true. A good selector with no match gives null.
  • Line 4 prints 0. The list from querySelectorAll is empty.
  • Line 6 uses a broken selector, li[, with no closing bracket. The browser throws an error instead of giving an empty result. try and catch catch the error, and line 8 prints its name: SyntaxError.
Which object has which method
  • getElementById("id") works on document. It does not work on ordinary elements. (It also works on DocumentFragment and ShadowRoot, two special node holders you'll meet later.)
  • getElementsByName("field") works only on document.
  • Every element has querySelector and querySelectorAll.

closest & matches

INTERACTIVE

Every element has two more selector methods:

  • matches(selector) asks one yes-or-no question: does this exact element match? It gives true or false.
  • closest(selector) checks the element itself first. If that doesn't match, it checks the parent, then the grandparent, and so on up the tree. It gives the first element that matches. If none match, it gives null.
Real-life analogyWalking up your own family line

An invitation arrives with a name on it. First you ask, "Is it me?" If not, you ask your parent, then your grandparent. You stop at the first person it names.

In real life: Are you the person named on the invitation?
In JavaScript: el.matches(selector)
In real life: If not, ask your parent, then your grandparent
In JavaScript: el.closest(selector)
In real life: No one in your family line is named
In JavaScript: null
element.closest with the selector li. Step 1 checks the clicked button itself: it is not an li. Step 2 checks its parent, li docs: it is an li, so closest returns it. The menu above is never checked.element.closest("li")ul#menunever checkedli docs2. an li? yesbutton1. an li? no
closest asks "is this an li?" about the clicked element first, then about each parent going up. It stops at the first yes.

This is very useful for event delegation. That means you put one click listener on a big container, instead of one listener on every button. Inside the listener, event.target is the exact element the click landed on. It might be an icon, a span, or the text inside a button. So you start from event.target and climb with closest to the button or card you care about.

closest and matches: climb up from a click
Climb up from the clicked elementPop out in the code editor (opens in a new tab)JavaScript
button.addEventListener("click", (event) => {  const element = event.target;  const isItem = element.matches("li");  const row = element.closest("li");  const menu = element.closest("#menu");});
Click an elementgreen = what closest gives
Try it yourself

Click any element in the mini page, then change the selector.

matches says yes or no about the element you clicked. closest checks that same element first, then each parent going up, until the selector matches.

Sometimes you also need to check that one element is inside another. a.contains(b) gives true when b is a itself or is somewhere inside a.

Check that an element is inside your cardPop out in the code editor (opens in a new tab)JavaScript
const card = document.querySelector("[data-card-id]");
const button = document.querySelector("#menu button");
console.log(card.contains(button));
console.log(button.contains(card));
console.log(document.contains(card));
  • Line 1 finds the card: the element with a data-card-id attribute. Line 2 finds the first button in the menu.
  • Line 3 prints true. The button is inside the card.
  • Line 4 prints false. The card is not inside the button. The order of the two matters.
  • Line 5 prints true. The card is on the page, so the document contains it.

Use contains when an element you found must belong to the part of the page you meant.

Older search methods: getElementsBy*

Before querySelector existed, browsers had methods such as getElementsByTagName and getElementsByClassName. They still work, and you will see them in older code.

The big difference is not how you describe what to find. It is the kind of list you get back. These methods give an HTMLCollection. That is a list of elements that is live: it updates by itself when the page changes.

How to read this table: each row is one search method. The last column says whether its result changes after you get it.

What each search method gives back
MethodWhat it gives backDoes the result change later?
document.querySelector(selector)The first matching element, or null.No. It is one element, not a list.
document.querySelectorAll(selector)A NodeList of every match.No. It is a snapshot, and it has forEach.
element.getElementsByTagName(name)A live HTMLCollection.Yes. It updates by itself. It has no forEach.
element.getElementsByClassName(name)A live HTMLCollection.Yes. It updates by itself. It has no forEach.
document.getElementsByName(name)A live NodeList of elements with that name attribute.Yes. It only works on document.

You can loop over an HTMLCollection with for...of. But it has no forEach, map, or filter, because it is not an array. Array.from(collection) copies it into a real array. Use that when you want array methods, or a copy that won't change.

Live vs static collections

INTERACTIVE
Real-life analogySecurity-camera feed vs photograph

A security camera shows the room right now. If someone walks in, the number of people on screen changes. A photograph shows the room at one moment. It stays the same even after the room changes.

In real life: A live security-camera feed
In JavaScript: A live HTMLCollection from getElementsBy*
In real life: A photograph taken once
In JavaScript: A static NodeList from querySelectorAll
In real life: People leave while you count them on the feed
In JavaScript: Removing items from a live list while you loop over it

Live lists are handy, but they cause a bug when you remove items while you loop forward. Here is what happens with three items:

  1. The loop starts at index 0 and removes the first item.
  2. The live list updates. The old second item slides down to index 0.
  3. The loop moves on to index 1. It never looks at index 0 again, so it skips the item that slid down.

Three safe fixes:

  • Copy the list first with Array.from(live), then loop over the copy.
  • Use querySelectorAll, which gives a snapshot.
  • Loop backward, from the last index to 0.
Live list or snapshot?
A live list and a snapshotPop out in the code editor (opens in a new tab)JavaScript
const liveItems = menu.getElementsByTagName("li");const staticItems = menu.querySelectorAll("li");menu.append(document.createElement("li"));console.log(liveItems.length);console.log(staticItems.length); for (const item of liveItems) {  item.remove();}
Listwatch the lengths
Try it yourself

Live length: 3. Static length: 3.

The live list from getElementsByTagName follows the page. The snapshot from querySelectorAll keeps what it saw when you called it.
A live list changes; a snapshot does notPop out in the code editor (opens in a new tab)JavaScript
const live = menu.getElementsByTagName("li");
const snapshot = menu.querySelectorAll("li");
menu.firstElementChild.remove();
console.log(live.length);
console.log(snapshot.length);
After line 3 removes li home: the live list holds two items, docs and shop, so live.length is 2. The snapshot from querySelectorAll still holds home, docs, and shop, so snapshot.length is 3.live.length: 2docsshoplivesnapshot.length: 3homedocsshopsnapshot
Line 3 removes the first li. The live list follows the page and now holds 2. The snapshot was taken before, so it still holds 3.

The menu starts with three li items. Line 1 gets a live list of them. Line 2 gets a snapshot of them. Line 3 removes the first item from the page.

  • Line 4 prints 2. The live list followed the page.
  • Line 5 prints 3. The snapshot still holds the three items it saw on line 2.

So a saved live list always shows the page as it is now. A saved snapshot can fall behind the page.

Live or static? Elements only or every node?
  • element.children
  • document.getElementsByClassName("done")
  • menu.getElementsByTagName("li")
  • menu.querySelectorAll("li")
  • element.childNodes
  • element.parentElement
  • button.closest("li")
  • document.querySelector(".done")
Try it yourself
0 of 8 correct

Put each property or method in the group that matches what it gives back.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Where you’ll use this

Real pages often start from the small element that was clicked. Then the code needs the bigger part of the page around it. For example:

  • In a to-do app, you click a delete icon. The code finds the to-do row around it.
  • In a menu, you click a span inside a button. The code finds the button.
  • In a product grid, you click "Save". The code finds the product card and reads its id.

The helper below does that last job. It uses three tools you may not know yet:

  • button.dataset.action reads the data-action attribute from the HTML.
  • card?.dataset uses ?. (optional chaining). If card is null, it stops and gives undefined instead of throwing an error.
  • ?? null means "if the left side is null or undefined, use null instead".
Find the clicked button and its cardPop out in the code editor (opens in a new tab)JavaScript
function findCardAction(event) {  const button = event.target.closest("button[data-action]");  if (!button) return null;   const card = button.closest("[data-card-id]");  return {    action: button.dataset.action,    cardId: card?.dataset.cardId ?? null,  };}

Line 1 takes the click event. Line 2 climbs from the clicked element to a button with a data-action attribute. Line 3 stops early with null when the click was not on such a button. Line 5 climbs again, to the card. Lines 6 to 9 give back the action and the card id.

This helper keeps working even if the button later gets an icon or an extra wrapper inside it. closest still climbs to the right button.

Tables have their own family links. A table element has rows and tBodies (its body sections), and each row has cells.

Walk a table with rows and cellsPop out in the code editor (opens in a new tab)JavaScript
const table = document.querySelector("#orders");
console.log(table.rows.length);
console.log(table.rows[0].cells.length);
console.log(table.tBodies[0].rows[1].cells[0].textContent);
  • Line 2 prints 3. rows holds the header row and the two body rows.
  • Line 3 prints 2. The first row has two cells.
  • Line 4 prints Toast. It takes the first body section, then its second row, then that row's first cell. You don't need a long selector for this.

Common misconceptions

  • "A child is always an element." Not always. firstChild can be a whitespace text node. Use firstElementChild to get a tag.
  • "A search with no match throws an error." No. A good selector with no match gives null or an empty list. Only a broken selector throws a SyntaxError.
  • "A NodeList is always static." No. querySelectorAll gives a static NodeList, but childNodes is a live NodeList.
  • "An HTMLCollection is an array." No. You can loop over it with for...of, but it has no array methods such as forEach.
  • "closest searches inside the element." No. It checks the element itself, then goes up through its parents. It never looks down.
  • "A search on an element only reads that element." No. The selector still looks at the whole page. Use :scope when the selector must start at the element you called it on.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict the two lengths

Type the exact output. It looks like the live and static lengths the playground shows.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const liveLength = 3;
const staticLength = 2;
console.log(liveLength + " / " + staticLength);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeChoose a selector for direct children

    Which selector finds the li elements whose parent is the element you search from?

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFind the bug in a removal loop

      This loop skips items. Name one safe way to fix it.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const items = list.getElementsByTagName("li");
      for (let i = 0; i < items.length; i += 1) {
        items[i].remove();
      }

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeName the method that climbs up

        Which method checks an element, then its parents, until a selector matches?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeWrite one click handler for the whole menu

          Write the handler in your editor or console. It should do nothing when the click is not on an action button.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          menu.addEventListener("click", (event) => {
            // Find a button with data-action, then find its li.
          });

            Check your understanding

            10 QUESTIONS
            Walking & searching quiz · 10 questionsScore: first tries count
            1. Question 1 of 10What does parentElement give when the parent is not an element?

              Choose an answer to see the explanation.

            2. Question 2 of 10Which property skips whitespace text nodes?

              Choose an answer to see the explanation.

            3. Question 3 of 10What does this print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const found = null;
              console.log(found === null);

              Choose an answer to see the explanation.

            4. Question 4 of 10Which sentence about querySelectorAll is true?

              Choose an answer to see the explanation.

            5. Question 5 of 10What does closest check first?

              Choose an answer to see the explanation.

            6. Question 6 of 10A buggy forward loop over a live list left two items behind. This array holds their labels. What does it print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const skipped = ["Home", "About"];
              console.log(skipped.length);

              Choose an answer to see the explanation.

            7. Question 7 of 10Which method works on document but not on ordinary elements?

              Choose an answer to see the explanation.

            8. Question 8 of 10With element.querySelectorAll, how do you find only the children whose parent is that element?

              Choose an answer to see the explanation.

            9. Question 9 of 10What does card.contains(button) tell you?

              Choose an answer to see the explanation.

            10. Question 10 of 10What does table.rows.length count?

              Choose an answer to see the explanation.

            Key takeaways

            • Use the properties with Element in their names (and children) when you want tags, not whitespace text nodes.
            • querySelector gives the first match or null. querySelectorAll gives a static NodeList of every match.
            • matches asks about one element. closest checks that element, then each parent going up.
            • getElementsByTagName, getElementsByClassName, and children give live lists that update by themselves.
            • Before you remove items from a live list in a loop, copy it with Array.from, or loop backward.

            DOM navigation is picking the right path through the page tree: follow family links when the node is close by, and search with a selector when you need to find matches.

            Up next: Node properties & contents.

            CompleteFrontend Clear concepts. Working examples.