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 before running code.
- 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: Root node representing the whole page and the entry point to its tree. — Source: WHATWG DOM: Document
- Node: A single object in the tree: document, element, text, 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: Direct containment relationship between two connected nodes. — Source: WHATWG DOM: Trees
- Ancestor/descendant: Indirect parent/child relationship spanning one or more levels. — Source: WHATWG DOM: Trees
- Sibling: Nodes sharing the same parent. — Source: WHATWG DOM: Trees
- Parser: Browser component converting 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
Mental Model: A Live Family Tree
HTML is the recipe sent to the browser. The DOM is the meal the browser constructs and keeps in memory. JavaScript does not normally edit the recipe file; it talks to the live model.
Consider this HTML:
<main>
<h1>My tasks</h1>
<ul>
<li>Read about the DOM</li>
</ul>
</main>
Its important element relationships are:
document └── html ├── head └── body └── main ├── h1 │ └── "My tasks" (text) └── ul └── li └── "Read about the DOM" (text)
main is the parent of h1 and ul. Those two elements are siblings. body is an ancestor of li; li is a descendant of body. An element is a node, but not every node is an element. Whitespace between tags can become text nodes, which is why childNodes may show more items than expected.
The browser may also repair HTML. For example, it supplies html, head, and body when they are omitted and can rearrange invalid table markup. Therefore, View Source shows the received HTML source, while the Elements panel shows the current, parsed DOM. They are related but not guaranteed to be identical.
Self-Study Example: Inspect a Todo Skeleton
Create one index.html file with the following complete page. defer tells the browser to execute the external script after the document has been parsed, so the elements exist before JavaScript inspects them.
<!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 step by step:
- Open the page through your development server.
- Open DevTools, then choose Elements. Expand
html,body,main,section, andul. - Double-click the first task's text in DevTools and change it. The page changes immediately.
- Reload. 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 element children only;childNodescan include 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.
document is not a copy of JavaScript. It is a browser-provided Web API object. JavaScript uses that object to observe and later change the page.
The First Architecture Preview
Interactive applications are easier to reason about when data and page output have distinct jobs:
state -> render -> user event -> update state -> render again
Today, the two <li> elements are written directly in HTML, so the DOM is acting as both content and display. Over the next lessons, an array will become the source of truth (state) and a render() function will make the DOM match it. The DOM is the view, not the database.
This distinction prevents a common bug: changing visible text but forgetting to change the underlying data. A later render would correctly use the data and apparently "undo" the direct DOM change.
Intermediate Example: Walk One Branch
The following code starts at the list and reports its element descendants. children deliberately ignores text and comment nodes.
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
This recursive function has a base case implicitly: an element with no element children makes the loop run zero times. Do not use recursion on an enormous unknown tree without limits, but it is a useful way to reveal the tree mental model.
Optional Advanced Example: Observe Live Changes
MutationObserver is the modern API for observing DOM changes. It is useful for debugging integrations, but ordinary app code should usually know when it changes its own state rather than observing its rendered 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. When finished, observer.disconnect() stops observation.
Deep Dive: DOM API Boundaries
The DOM is a browser-provided object model, not part of the ECMAScript core language. JavaScript interacts with it through standardized browser APIs.
A useful hierarchy is:
window └── document └── html ├── head └── body └── ...
window represents the browser browsing context. document represents the loaded document.
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 distinction explains many surprising traversal results.
Mistakes and Debugging
- Calling the DOM "JavaScript": JavaScript is the language; the DOM is a Web API also used by scripts.
- 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 proves whether a change came from source/state or only the current DOM.
- Getting
nullfrom a selector: check 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().
Debug systematically: reproduce the issue, inspect the Elements panel, log the exact node, verify its parent/children, then compare source and live DOM. Avoid random edits until something works.
Accessibility, Security, and Performance
Accessibility: DOM order is meaningful. Screen readers and keyboard users generally encounter content according to document and focus order, so do not use CSS to create a visual order that contradicts the DOM. Semantic elements (main, headings, lists, buttons) expose useful relationships without extra ARIA. The page has a language, title, heading hierarchy, and a real list.
Security: Inspecting or modifying the DOM is not automatically unsafe. The danger arrives 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. For two tasks this is irrelevant; for 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 h2's parent. 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.
