Walking the DOM

The DOM lets you do anything with a page’s elements and their contents. But before you can change a node, you have to reach it. That’s what this chapter is about: moving around the tree from one node to another.

Every DOM operation begins at the document object. It’s the entry point — the handle the browser hands you to the whole page. From document, a web of properties lets you hop to any node: up to a parent, down to children, sideways to siblings.

elemparentNodepreviousSiblingnextSiblingfirstChildlastChild
Every node exposes links to its neighbors. From any node you can climb up, drop down, or step sideways.

Let’s walk through each of those links.

On top: documentElement and body

The nodes near the top of the tree are so common that document exposes them directly, no searching required.

<html> = document.documentElement
The root of the whole tree. It’s the DOM node for the <html> tag, and the topmost element you can reach.

<body> = document.body
The node for <body>, where most visible content lives. You’ll use this one constantly.

<head> = document.head
The node for the <head> tag.

Children: childNodes, firstChild, lastChild

Two words come up over and over, so let’s pin them down:

  • Child nodes (or children) are the direct descendants — the nodes nested exactly one level inside. <head> and <body> are children of <html>.
  • Descendants are everything nested inside, at any depth: children, their children, and so on down the tree.

Take this markup:

<html>
<body>
  <div>Title</div>

  <ul>
    <li>
      <b>Author</b>
    </li>
  </ul>
</body>
</html>

Here <body> has two element children, <div> and <ul> (plus a few blank text nodes from the whitespace between tags). Its descendants go further: <li> sits inside <ul>, and <b> sits inside <li>. The whole subtree counts.

bodydivullibchildren of bodydeeper descendants
Children are one level down. Descendants are the entire subtree beneath a node.

childNodes gives you every child node — including text and comment nodes.

Here’s a loop over the children of document.body:

<html>
<body>
  <div>Title</div>

  <ul>
    <li>Author</li>
  </ul>

  <div>Price</div>

  <script>
    for (let i = 0; i < document.body.childNodes.length; i++) {
      alert( document.body.childNodes[i] ); // Text, DIV, Text, UL, ..., SCRIPT
    }
  </script>
  ...more stuff...
</body>
</html>

Notice the last node the loop reports is the <script> itself. There’s more HTML below it (...more stuff...), but at the moment the script runs the browser hasn’t parsed that part yet — so those later nodes simply aren’t in the DOM to be seen. Same building-as-it-reads behavior as the document.body gotcha above.

firstChild and lastChild are quick shortcuts to the ends of that list.

They’re pure convenience. When children exist, these two identities always hold:

elem.childNodes[0] === elem.firstChild
elem.childNodes[elem.childNodes.length - 1] === elem.lastChild

And to check whether a node has any children at all, there’s elem.hasChildNodes(), which returns a boolean.

DOM collections

childNodes looks like an array — it has a length, you can index into it with [i]. But it isn’t a real array. It’s a collection: an array-like, iterable object. That distinction has two practical consequences.

1. You can iterate it with for..of. Collections implement Symbol.iterator, so the loop works cleanly:

for (let node of document.body.childNodes) &#123;
  alert(node); // shows every node in the collection
&#125;

2. Array methods are missing. Because it isn’t an Array, methods like filter, map, and forEach aren’t there:

alert(document.body.childNodes.filter); // undefined — no filter method

The first point is genuinely useful. The second is easy to work around: wrap the collection with Array.from when you want real array methods.

alert( Array.from(document.body.childNodes).filter ); // function

Siblings and the parent

Siblings are nodes that share the same parent. In this markup, <head> and <body> are siblings — both children of <html>:

<html>
  <head>...</head><body>...</body>
</html>
  • <body> is the next (right) sibling of <head>.
  • <head> is the previous (left) sibling of <body>.

You reach them through nextSibling and previousSibling. The parent is parentNode.

// parent of <body> is <html>
alert( document.body.parentNode === document.documentElement ); // true

// after <head> comes <body>
alert( document.head.nextSibling ); // HTMLBodyElement

// before <body> comes <head>
alert( document.body.previousSibling ); // HTMLHeadElement

The quickest way to build intuition for these links is to walk them. Start on a highlighted node in the little tree below, then press the buttons to hop to its parent, siblings, or first child. Each move uses the element-only property with the same name, and when a move lands on nothing the walker tells you the property returned null.

interactiveWalking the tree, one step at a time

Element-only navigation

Everything above walks over all node types. childNodes, nextSibling, and their kin will happily hand you text nodes, comment nodes, and element nodes alike.

Often that’s not what you want. For most work you care only about element nodes — the ones that stand for tags and give the page its structure. The whitespace text nodes between tags just get in the way.

So there’s a parallel set of properties that skips everything except elements. Same idea, with Element in the name:

  • children — only the children that are element nodes.
  • firstElementChild, lastElementChild — the first and last element children.
  • previousElementSibling, nextElementSibling — the neighboring elements.
  • parentElement — the parent element.
all nodes
parentNode
childNodes
firstChild / lastChild
previousSibling / nextSibling
elements only
parentElement
children
firstElementChild / lastElementChild
previousElementSibling / nextElementSibling
The all-nodes links (left) versus the element-only links (right). Text and comment nodes are skipped on the right.

Swap childNodes for children in the earlier example, and now only elements show up — the blank text nodes are gone:

<html>
<body>
  <div>Title</div>

  <ul>
    <li>Author</li>
  </ul>

  <div>Price</div>

  <script>
    for (let elem of document.body.children) {
      alert(elem); // DIV, UL, DIV, SCRIPT
    }
  </script>
  ...
</body>
</html>

Seeing the difference for yourself makes it stick. The box below holds a <span>, some whitespace, a comment, and a <b>. Toggle between the two properties and watch how childNodes reports the invisible text and comment nodes while children keeps only the elements.

interactivechildNodes vs children

The properties so far apply to any node. On top of them, some element types add their own convenience properties tuned to their structure. Tables are the standout example — navigating rows and cells by index is far nicer than crawling through children.

A <table> element adds:

  • table.rows — the collection of all <tr> in the table.
  • table.caption / table.tHead / table.tFoot — references to <caption>, <thead>, <tfoot>.
  • table.tBodies — the collection of <tbody> elements. A table can have several per the spec, and there’s always at least one: even if you leave <tbody> out of your HTML, the browser inserts it into the DOM.

<thead>, <tfoot>, <tbody> each add:

  • .rows — the <tr> elements inside that section.

<tr> adds:

  • tr.cells — the <td> and <th> cells in that row.
  • tr.sectionRowIndex — the row’s index within its own <thead> / <tbody> / <tfoot>.
  • tr.rowIndex — the row’s index across the whole table, counting every row.

<td> and <th> add:

  • td.cellIndex — the cell’s index within its <tr>.
rows[0]rows[1]cells[1]applepearplumlimecells[0] cells[1]
Reaching one cell by index: table.rows[0].cells[1] walks to row 0, then cell 1.

Putting those together to grab one cell:

<table id="table">
  <tr>
    <td>apple</td><td>pear</td>
  </tr>
  <tr>
    <td>plum</td><td>lime</td>
  </tr>
</table>

<script>
  // get the td holding "pear" (first row, second column)
  let td = table.rows[0].cells[1];
  td.style.backgroundColor = "red"; // highlight it
</script>

Try it live. Pick a row and a column, and the demo runs table.rows[row].cells[col] for you — highlighting the cell and reporting the very indexes we just met: rowIndex and cellIndex.

interactiveReaching a cell by index

Full details live in the tabular data specification.

HTML forms come with their own extra navigation properties too. We’ll get to those when we start working with forms.

Summary

From any DOM node you can step to its immediate neighbors with navigation properties. There are two parallel sets:

  • All nodes (text and comments included): parentNode, childNodes, firstChild, lastChild, previousSibling, nextSibling.
  • Element nodes only: parentElement, children, firstElementChild, lastElementChild, previousElementSibling, nextElementSibling.

Reach for the element-only set when whitespace text nodes would just be noise. And remember a few sharp edges: these properties are read-only, most collections are live, loop them with for..of, and certain elements like tables layer on their own shortcuts for getting at rows and cells.