1The DOM Tree Structure
▶
When a browser receives an HTML file, it does not display the raw markup text directly. Instead, it reads through the markup character by character and constructs a rich, interconnected data structure entirely in memory. This data structure is called the Document Object Model, or DOM, and its shape is that of a tree — a hierarchical arrangement of linked objects called nodes. Understanding this tree is the foundation of all JavaScript-based web development, because every interaction a script has with a web page — reading content, changing styles, responding to clicks, injecting new elements — is really an interaction with this in-memory tree, not with the HTML text file itself.
The tree metaphor is precise and intentional. Just like a botanical tree, the DOM tree has a single root from which everything else branches outward. Each branch point is a node, and nodes are connected to one another through clearly defined relationships: a node can be a parent of the nodes below it, a child of the node above it, and a sibling of nodes that share the same parent. These relationships mirror exactly the nesting structure of the original HTML tags. A <p> element nested inside a <div> becomes a child node of the <div> node. Text written between an opening and closing <p> tag becomes a child node of that <p> node. The tree captures every piece of the document — its structure, its content, and its metadata — in a form that programs can traverse and modify.
There are several distinct types of nodes in the DOM tree. The most important are the document node, element nodes, text nodes, and attribute nodes. Each type has a specific role, and together they account for nearly everything a developer works with when scripting a web page.
The Document Node
At the very top of every DOM tree sits a single, unique object: the document node. There is exactly one document node per page, and it is the root ancestor of every other node in the entire tree. In JavaScript, this node is always available through the global variable document. It does not correspond to any visible HTML element — you will not see a <document> tag in your markup — but it represents the HTML document as a whole, serving as the entry point to everything inside it.
Because the document node is the root, all DOM queries begin there. When you write document.getElementById('main') or document.querySelector('.highlight'), you are asking the document node to search its descendant tree for matching nodes. Without this single, stable starting point, navigating the tree would have no reliable origin. Every node in the tree can ultimately trace its lineage back through parent relationships to the document node.
The document node also exposes properties that describe the document itself, such as document.title, document.URL, and document.doctype. These are attributes of the document as a whole, not of any particular element within it.
Element Nodes
Every HTML tag you write in your markup — <html>, <body>, <div>, <p>, <a>, <ul>, <li>, and so on — becomes an element node in the DOM tree. Element nodes are by far the most common node type a developer interacts with, and they form the skeleton of the tree.
The nesting of element nodes in the DOM directly reflects the nesting of tags in the HTML source. Consider this markup:
<div id="container">
<h1>Hello, World</h1>
<p>Welcome to the DOM.</p>
</div>
In the DOM tree, the <div> element node is the parent of both the <h1> element node and the <p> element node. The <h1> and <p> nodes are siblings of each other, because they share the same parent. The <div> itself is a child of the <body> element node, which is a child of the <html> element node, which is a child of the document node.
Element nodes support a rich set of JavaScript properties and methods. You can read or change an element's innerHTML, textContent, style, and classList. You can call appendChild(), removeChild(), and replaceChild() to restructure the tree. When developers talk about "selecting an element" or "manipulating the DOM," they are almost always working with element nodes.
Text Nodes
Between the opening and closing tags of an element, any raw string of characters — including ordinary words, numbers, punctuation, and even whitespace — becomes a text node. Text nodes are always children of element nodes, and they are always leaves of the tree, meaning they have no children of their own. They sit at the outermost edges of the DOM tree.
In the example above, the string Hello, World inside the <h1> tag is not stored directly on the <h1> element node. It is stored in a separate text node that is a child of the <h1> element node. The same is true of Welcome to the DOM. inside the <p> tag.
This distinction matters when navigating the tree programmatically. If you access h1Element.childNodes, you will find a collection that includes the text node, not just child elements. If you want to read or change the visible text, you can access the text node's nodeValue property, or more conveniently use the element's textContent property, which collects the text content of the element and all its descendants.
Whitespace in HTML — newlines and spaces between tags — also creates text nodes, which can sometimes surprise developers who are iterating over childNodes and encounter unexpected entries. Using children (which returns only element nodes) instead of childNodes (which returns all node types) is a common way to avoid this.
Attribute Nodes
HTML elements carry attributes — pieces of metadata written directly in the opening tag, such as id="container", class="highlight", href="https://example.com", or src="image.png". Each such attribute is modeled in the DOM as an attribute node.
Attribute nodes occupy a special position in the DOM. Unlike element nodes and text nodes, attribute nodes are not considered children of their element in the standard parent-child sense. They are treated as properties that belong to their element node, rather than nodes that hang below it in the tree hierarchy. You will not encounter attribute nodes when walking childNodes or children. Instead, they are accessed through dedicated APIs on the element node itself.
JavaScript provides several methods for working with attribute nodes:
element.getAttribute('class')— reads the value of the named attribute, returning it as a string.element.setAttribute('class', 'new-value')— sets or updates the named attribute to the given value.element.removeAttribute('class')— removes the attribute entirely from the element.element.hasAttribute('class')— returnstrueif the attribute exists,falseotherwise.
Many commonly used attributes also have direct JavaScript property equivalents on the element node. For instance, element.id, element.className, element.href, and element.src provide shorthand access to their corresponding HTML attributes. The element.classList API goes further, exposing methods like add(), remove(), and toggle() for managing CSS classes without manipulating raw strings.
Parent, Child, and Sibling Relationships
The power of the DOM tree comes largely from the navigational relationships between nodes. These relationships allow a developer starting at any node to move to any other node in the tree, traveling upward toward ancestors, downward toward descendants, or sideways to siblings.
| Relationship | Definition | JavaScript Property |
|---|---|---|
| Parent | The node directly above the current node in the hierarchy | node.parentNode / element.parentElement |
| Child | A node directly below the current node | node.childNodes / element.children |
| First Child | The first node in the current node's list of children | node.firstChild / element.firstElementChild |
| Last Child | The last node in the current node's list of children | node.lastChild / element.lastElementChild |
| Next Sibling | The node immediately after the current node, sharing the same parent | node.nextSibling / element.nextElementSibling |
| Previous Sibling | The node immediately before the current node, sharing the same parent | node.previousSibling / element.previousElementSibling |
| Ancestor | Any node above the current node (parent, grandparent, etc.) | Traversed via repeated .parentNode calls or closest() |
| Descendant | Any node below the current node at any depth | element.querySelectorAll() searches all descendants |
Notice that most relationships have two flavors of property: one that accounts for all node types (including text nodes and comment nodes), and one suffixed with Element that considers only element nodes. In practice, the Element variants are more commonly useful, because developers usually want to navigate between tags rather than raw text strings.
To illustrate these relationships concretely, consider the following HTML:
<ul id="list">
<li>First</li>
<li>Second</li>
<li>Third</li>
</ul>
In the DOM, the <ul> node is the parent of three <li> nodes. Each <li> node is a child of the <ul>. The three <li> nodes are siblings of each other. The text First is a text node child of the first <li>. The first <li> has no previous sibling, and the third <li> has no next sibling. Using JavaScript:
const list = document.getElementById('list');
// Access children
const items = list.children; // HTMLCollection of three <li> elements
const first = list.firstElementChild; // the first <li>
const last = list.lastElementChild; // the third <li>
// Sibling navigation from the first item
const second = first.nextElementSibling; // the second <li>
// Move upward
const parentOfList = list.parentElement; // the <body> element (or enclosing container)
How Browsers Build the DOM Tree from HTML
The DOM tree does not appear all at once. It is built incrementally by the browser's HTML parser as it reads the source file from top to bottom. Understanding this construction process illuminates both how the tree gets its shape and why the order of elements in HTML matters.
The process works as follows:
- The parser begins by creating the document node, which immediately becomes the root of the tree.
- As the parser reads the first opening tag —
<html>— it creates a corresponding element node and attaches it as a child of the document node. The parser then descends into this node, meaning subsequent nodes will be created as children of<html>. - When the parser encounters the next opening tag — say,
<head>— it creates a<head>element node as a child of<html>and descends again. - When it encounters a closing tag —
</head>— it moves back up to the parent node (<html>), signaling that the<head>subtree is complete. - Character data found between tags is used to create text nodes, attached as children of the current element node.
- This process of descend-on-open, ascend-on-close continues recursively through the entire document, perfectly reproducing the nested structure of the HTML as a tree of linked node objects.
By the time the parser reaches the end of the HTML file, the full DOM tree exists in memory. This completed tree is what JavaScript interacts with at runtime. A script can call document.createElement('p') to create a new element node, set its textContent, and then call someParent.appendChild(newParagraph) to insert it into the tree. The browser will immediately update what is displayed on screen to reflect this change.
Crucially, these changes to the DOM tree are entirely in memory. The original .html file sitting on the server is never touched. If you reload the page, the browser re-parses the original file and builds a fresh tree, discarding any modifications made by JavaScript during the previous session. The DOM is a live representation of the current state of the document in the browser, not a persistent record of the file on disk.
This also explains why JavaScript placed at the top of an HTML file — before the rest of the page is parsed — can fail to find elements that are defined lower in the markup. At the moment that script executes, those elements have not yet been parsed into the DOM tree. Placing scripts at the bottom of the <body> or using the defer attribute on <script> tags ensures the full tree is available before code runs.