068: DOM Fundamentals and Browser DOM APIs
Outcomes
By the end of this lesson, you can:
- explain how the browser turns HTML into a live DOM tree;
- distinguish a document, node, element, and text node;
- describe parent, child, sibling, ancestor, and descendant relationships;
- inspect the live DOM in DevTools; and
- explain why changing the DOM does not rewrite the original HTML file.
Retrieval Warm-Up
Answer these questions before you run any code. The goal is to bring the relevant HTML and browser concepts to the front of your mind.
- What roles do HTML, CSS, and JavaScript usually play in a page?
- Which HTML element should contain the page's main heading?
- What does nesting one HTML element inside another communicate?
Terms
- DOM: “The DOM connects web pages to scripts by representing the structure of a document in memory.” — Source: MDN: Document Object Model
- Document: The root node representing the whole page and the entry point to its tree. — Source: WHATWG DOM: Document
- Node: A single object in the tree: a document, element, text node, or comment. — Source: WHATWG DOM: Nodes
- Element: A node corresponding to a markup tag within the document tree. — Source: WHATWG DOM: Elements
- Text node: A node containing character data between or inside elements. — Source: WHATWG DOM: Text
- Parent/child: A direct containment relationship between two connected nodes. — Source: WHATWG DOM: Trees
- Ancestor/descendant: An indirect parent/child relationship spanning one or more levels. — Source: WHATWG DOM: Trees
- Sibling: Nodes that share the same parent. — Source: WHATWG DOM: Trees
- Parser: The browser component that converts HTML source bytes into the DOM tree. — Source: WHATWG HTML: Parsing
- DOM tree (official): "The DOM is a tree-like representation of the document, where each node is an object representing part of the document." — Source: MDN: Document Object Model
- Tree (official): "A tree is a finite hierarchical structure with a root and children; a node may have parent, children, siblings." — Source: WHATWG DOM: Trees
- Live collection: "A collection that automatically updates when the document changes." — Source: MDN: NodeList — Live vs Static
These terms are closely related, but they are not interchangeable. In particular, remember the useful boundary: every element is a node, while text and comment nodes are nodes that are not elements.
Mental Model: A Live Family Tree
HTML is the recipe the browser receives. The DOM is the in-memory result the browser constructs from that recipe and keeps available to the page. JavaScript normally talks to this live model; it does not edit the recipe file on disk.
Consider this HTML:
<main>
<h1>My tasks</h1>
<ul>
<li>Read about the DOM</li>
</ul>
</main>
The important element relationships look like this:
document
└── html
├── head
└── body
└── main
├── h1
│ └── "My tasks" (text)
└── ul
└── li
└── "Read about the DOM" (text)
main is the parent of h1 and ul, and those two elements are siblings because they share main as their parent. body is an ancestor of li, while li is a descendant of body. An element is one kind of node, but an element is not the same thing as every node. Whitespace between tags can also become text nodes, which is why childNodes often contains more entries than a quick visual reading of the markup suggests.
There is another detail that matters when you inspect real pages: the browser can repair HTML. It supplies html, head, and body when they are omitted, and it can rearrange invalid table markup. As a result, View Source shows the HTML source that was received, whereas the Elements panel shows the current parsed DOM. They describe the same page, but they are not guaranteed to be identical.
Self-Study Example: Inspect a Todo Skeleton
Create a single index.html file containing this complete page. The defer attribute tells the browser to wait until the document has been parsed before executing the external script. That timing matters because the elements will already exist when JavaScript looks them up.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DOM tree explorer</title>
<script src="app.js" defer></script>
</head>
<body>
<header>
<h1>Todo learning app</h1>
</header>
<main id="app">
<section aria-labelledby="tasks-heading">
<h2 id="tasks-heading">Tasks</h2>
<p id="summary">2 tasks</p>
<ul id="task-list">
<li>Inspect the DOM</li>
<li>Draw the tree</li>
</ul>
</section>
</main>
</body>
</html>
Create app.js beside it:
console.log(document);
console.log(document.documentElement);
console.log(document.body);
const app = document.querySelector("#app");
console.log("Element:", app);
console.log("Parent element:", app.parentElement);
console.log("Element children:", app.children);
console.log("All child nodes:", app.childNodes);
Investigate it in this order:
- Open the page through your development server.
- Open DevTools and choose Elements. Expand
html,body,main,section, andul. - Double-click the first task's text in DevTools and change it. The page should change immediately.
- Reload the page. Your edit disappears because it changed only the in-memory DOM, not
index.html. - Open Console. Expand
document, theappelement,app.children, andapp.childNodes. - Compare
childrenwithchildNodes.childrencontains only element children;childNodescan also contain whitespace text nodes. - Right-click the page and choose View page source. Compare that stable source with the editable Elements panel.
Point to the li node and trace upward to document. Then identify the sibling of h2, the parent of ul, and two ancestors of the first text node.
One conceptual boundary is worth making explicit: document is not a copy of JavaScript. It is a browser-provided Web API object. JavaScript uses that object to inspect the document and, later, to change the page.
The First Architecture Preview
Interactive applications become easier to reason about when the data that drives a page and the page output itself have separate jobs:
state -> render -> user event -> update state -> render again
In this lesson, the two <li> elements are written directly in HTML, so the DOM is serving as both the content and the display. In later lessons, an array will become the source of truth, or state, and a render() function will make the DOM match that state. The DOM is the view, not the database.
That separation prevents a common bug: changing visible text while forgetting to update the underlying data. When a later render uses the unchanged data, it will correctly produce the old value and appear to have undone the direct DOM edit.
Intermediate Example: Walk One Branch
The following code starts at the list and reports its element descendants. It uses children deliberately, so whitespace and comment nodes are excluded from this traversal.
const taskList = document.querySelector("#task-list");
function printElementTree(element, depth = 0) {
console.log(`${" ".repeat(depth)}${element.localName}`);
for (const child of element.children) {
printElementTree(child, depth + 1);
}
}
printElementTree(taskList);
Expected console output:
ul
li
li
The recursive function has an implicit base case: when an element has no element children, the loop runs zero times and that branch ends. Recursion is a useful way to expose the tree model, although you should not apply it to an enormous unknown tree without appropriate limits.
Optional Advanced Example: Observe Live Changes
MutationObserver is the modern API for observing changes to the DOM. It is useful when debugging an integration or a component you do not control. In ordinary application code, however, the code that changes state should usually already know when it is rendering, rather than discovering changes by observing its own output.
const observer = new MutationObserver((records) => {
for (const record of records) {
console.log(record.type, record.target);
}
});
observer.observe(document.querySelector("#task-list"), {
childList: true,
subtree: true,
characterData: true,
});
Edit a task in DevTools and inspect the records that appear. When you finish, call observer.disconnect() to stop observing.
Deep Dive: DOM API Boundaries
The DOM is a browser-provided object model, not part of the ECMAScript core language. JavaScript reaches it through standardized browser APIs. This distinction matters when code runs somewhere other than a browser: the JavaScript language is still available, but browser objects such as document may not be.
A useful hierarchy is:
window
└── document
└── html
├── head
└── body
└── ...
window represents the browser browsing context, while document represents the loaded document inside that context.
console.log(window.location.href);
console.log(document.title);
Node versus Element
Every Element is a Node, but not every Node is an Element. Text nodes and comments are nodes too.
const card = document.querySelector(".card");
console.log(card.nodeType);
console.log(card.children); // element children
console.log(card.childNodes); // all child nodes
This boundary explains many traversal results that initially look surprising. Use an element-oriented API when you want elements, and use childNodes when text and comments are part of the question.
Mistakes and Debugging
- Calling the DOM "JavaScript": JavaScript is the language. The DOM is a Web API that scripts can use.
- Assuming every node is an element: Text and comment nodes are nodes too. Use
childrenfor element-only traversal andchildNodeswhen every node matters. - Counting whitespace unexpectedly: Formatting line breaks can create text nodes. Inspect
nodeTypeor use element-oriented APIs. - Expecting DevTools edits to persist: Reload to test whether a change came from source or state, rather than only from the current DOM.
- Getting
nullfrom a selector: Check the spelling and script timing. Usedefer, place the script after the markup, or wait forDOMContentLoadedwhen you cannot control loading. - Using
document.write(): Avoid it. It can replace a loaded document and interacts poorly with modern page loading. Later lessons usecreateElement()andappend()instead.
Debug systematically: reproduce the issue, inspect the Elements panel, log the exact node, verify its parent and children, and then compare the source with the live DOM. Random edits may eventually appear to fix something, but they do not tell you which boundary caused the problem.
Accessibility, Security, and Performance
Accessibility: DOM order carries meaning. Screen readers and keyboard users generally encounter content according to document order and focus order, so do not use CSS to create a visual order that contradicts the DOM. Semantic elements such as main, headings, lists, and buttons expose useful relationships without extra ARIA. This page has a language, a title, a heading hierarchy, and a real list.
Security: Inspecting or modifying the DOM is not inherently unsafe. The risk appears when untrusted text is parsed as markup or code. Beginning in 070, task text will be assigned with textContent, not inserted as arbitrary HTML.
Performance: DOM reads and writes have costs because rendering may require style, layout, and paint work. With two tasks, that cost is irrelevant; with thousands, repeated scattered updates can become visible. Keep state separate, render deliberately, and measure before optimizing.
Exercises
Core
Draw the element tree from body downward. Label the parent of h2, the sibling of h2, and the descendants of ul.
Practice
Add a <footer><p>Learning DOM basics</p></footer> after main. Predict the relationships before checking DevTools.
Professional Extension
Change printElementTree so each line includes an element's id when present, such as ul#task-list.
Core
body
├── header
│ └── h1
└── main#app
└── section
├── h2#tasks-heading
├── p#summary
└── ul#task-list
├── li
└── li
The section is the parent of h2. p and ul are its siblings. The li elements and their text nodes are descendants of ul.
Practice
</main>
<footer>
<p>Learning DOM basics</p>
</footer>
main and footer are siblings and children of body. p is a child of footer and a descendant of body.
Professional Extension
function printElementTree(element, depth = 0) {
const identity = element.id ? `#${element.id}` : "";
console.log(`${" ".repeat(depth)}${element.localName}${identity}`);
for (const child of element.children) {
printElementTree(child, depth + 1);
}
}
printElementTree(document.body);
Recap
- The browser parses HTML into a live tree of DOM nodes.
documentrepresents the loaded document.- Elements are nodes, but text and comments are nodes too.
- Tree vocabulary makes selection, events, and rendering easier to explain.
- Source HTML and the current DOM can differ.
- Our app will follow
state -> render -> event -> update -> render.
