---
title: "11 — DOM APIs — Talking to the Page from JavaScript"
uid: dom-apis
tags: ["web-apis", "browser", "roadmap:javascript", "manipulation", "dom", "javascript", "events"]
excerpt: "The DOM is a live tree the browser builds from HTML — and JavaScript borrows methods from the browser to query, change, and listen to that tree. Not part of the language."
date: 2026-08-13T03:28:05+0000
source: https://www.aveshina.my.id/en/blog/dom-apis
---

"The document object" was my DOM model, and it conflated the tree with the language that touches it. The idea that everything else hangs off: **the DOM is a live tree of nodes the browser builds from the HTML, and JavaScript doesn't own it — it borrows a set of methods from the browser's host environment to query, change, and listen to that tree.** [1]

The framing that finally landed is the host-environment split. The ECMAScript spec defines the core language (types, functions, closures). The DOM is specified separately — by the WHATWG/Web APIs — and *provided by the browser*. document, window, querySelector, addEventListener are not JavaScript. They're the browser's API surface, handed to the JS engine so my code can touch the page. That distinction explains why the same language in Node has no document — Node ships different host APIs.

```figure
<svg viewBox="0 0 740 280" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="HTML source is parsed by the browser into a DOM tree. JavaScript reaches into that tree through host API methods: querySelector to find a node, addEventListener to react to events, createElement/appendChild to change structure.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- HTML source -->
    <rect x="20" y="40" width="160" height="120" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="100" y="60" font-size="11" font-weight="700" fill="#422006" text-anchor="middle">HTML source</text>
    <g font-size="10" font-family="ui-monospace,monospace" fill="#422006">
      <text x="32" y="85">&lt;html&gt;</text>
      <text x="42" y="103">&lt;body&gt;</text>
      <text x="52" y="121">&lt;h1&gt;Hi&lt;/h1&gt;</text>
      <text x="52" y="139">&lt;button/&gt;</text>
    </g>

    <!-- arrow -->
    <path d="M185,100 L235,100" stroke="#64748b" stroke-width="1.5" marker-end="url(#darrow)"/>

    <!-- DOM tree -->
    <rect x="240" y="20" width="290" height="200" rx="10" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="385" y="42" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">DOM tree (live, in memory)</text>
    <g font-size="10" font-family="ui-monospace,monospace" fill="#1e1b4b" text-anchor="middle">
      <rect x="345" y="55" width="80" height="26" rx="5" fill="#fff" stroke="#6366f1"/><text x="385" y="72">html</text>
      <rect x="345" y="100" width="80" height="26" rx="5" fill="#fff" stroke="#6366f1"/><text x="385" y="117">body</text>
      <rect x="270" y="145" width="80" height="26" rx="5" fill="#fff" stroke="#6366f1"/><text x="310" y="162">h1 "Hi"</text>
      <rect x="420" y="145" width="90" height="26" rx="5" fill="#fff" stroke="#6366f1"/><text x="465" y="162">button</text>
      <line x1="385" y1="81" x2="385" y2="100" stroke="#94a3b8"/>
      <line x1="385" y1="126" x2="310" y2="145" stroke="#94a3b8"/>
      <line x1="385" y1="126" x2="465" y2="145" stroke="#94a3b8"/>
    </g>

    <!-- JS engine -->
    <rect x="570" y="100" width="160" height="80" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="650" y="128" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">JavaScript engine</text>
    <text x="650" y="146" font-size="9.5" fill="#475569" text-anchor="middle">calls host API methods</text>
    <text x="650" y="162" font-size="9.5" font-style="italic" fill="#64748b" text-anchor="middle">querySelector · listen · create</text>
    <path d="M570,140 L532,140" stroke="#64748b" stroke-width="1.5" marker-end="url(#darrow)"/>
    <path d="M532,150 L568,150" stroke="#64748b" stroke-width="1.5" marker-end="url(#darrow)"/>

    <defs>
      <marker id="darrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
        <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
      </marker>
    </defs>
  </g>
</svg>
```

## The tree, and how I find nodes

The browser parses HTML into a tree of **nodes** — element nodes, text nodes, comment nodes — rooted at document. Almost everything I do starts with *finding* the node I want [1]:

```
document.querySelector("#submit");        // first match by CSS selector
document.querySelectorAll(".item");        // all matches (a NodeList)
document.getElementById("submit");         // by id (legacy, fast)
element.closest(".card");                  // nearest ancestor matching a selector
```

querySelector / querySelectorAll are the modern, universal tools — they take any CSS selector, so the same syntax I use for styling works for querying. The legacy methods (getElementById, getElementsByClassName) still exist and are slightly faster, but I rarely need the speed.

## Changing the tree

Once I have a node, the API for reading and mutating it is large but the common moves are few [1]:

```
const btn = document.querySelector("#submit");
btn.textContent = "Save";                  // change text
btn.setAttribute("disabled", "");          // change an attribute
btn.classList.add("loading");              // toggle a class
btn.style.color = "red";                   // inline style (prefer classes)

// structure changes
const item = document.createElement("li"); // make a node
item.textContent = "New";
list.appendChild(item);                    // attach it
list.removeChild(oldItem);                 // remove one
oldItem.replaceWith(newItem);              // swap one
```

Two things to keep straight. First, textContent vs innerHTML — textContent sets plain text safely; innerHTML parses HTML, which is both more powerful and an XSS vector if the content comes from user input. Second, prefer CSS classes over inline style writes — classes keep styling concerns in stylesheets, where they belong.

## Listening to events

Interactivity is events. The pattern is addEventListener(event, callback) — register a function to run when something happens [1]:

```
btn.addEventListener("click", (event) => {
  console.log(event.target);   // the element that was clicked
  doSave();
});
```

The callback receives an **event object** with details: event.target (the actual element clicked), event.currentTarget (the element the listener is attached to), event.preventDefault() (stop the default action), event.stopPropagation() (stop bubbling). Events **bubble** from the target up to the root, which is why **event delegation** works — one listener on a parent handles clicks on any of its children, useful when children are added dynamically.

## Beyond the DOM: other Web APIs

The DOM is the headline, but browsers expose many more Web APIs that JavaScript can call [1]: **Web Storage** (localStorage, sessionStorage) for key-value persistence, **Fetch** for network requests, **History** for URL manipulation without reload, **Geolocation**, **Canvas** and **WebGL** for graphics, **Web Workers** for background threads. Each is a host API — not part of the language, lent by the browser, and absent in Node unless polyfilled.

## How I use this

In a framework world (React, Vue, Svelte), I touch the DOM directly far less than I used to — the framework owns the tree and I declare state, not nodes. But the DOM APIs are still the substrate underneath, and they're essential for: reading a value the framework doesn't manage (a third-party widget, a measurement), integrating non-framework code, or building something without a framework. The rule I keep: reach for querySelector over legacy methods, prefer textContent and classes over innerHTML and inline styles, and use event delegation when children are dynamic. And remember that none of these methods are JavaScript — they're the browser's surface, and they won't exist in a Node script.

## References

[1] Mozilla, "Document Object Model (DOM)," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model)

[2] Mozilla, "Introduction to events," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events)

[3] Mozilla, "Element.querySelector()," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)

[4] Mozilla, "EventTarget.addEventListener()," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)

[5] Mozilla, "Web APIs — list of interfaces," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/API](https://developer.mozilla.org/en-US/docs/Web/API)

```quiz
Q: The DOM methods like querySelector and addEventListener are…
- part of the ECMAScript language spec
- host APIs provided by the browser, not part of the core language
correct: 1
explain: The DOM is a separate specification, implemented by the browser. The JS engine calls these methods through the host environment. A Node.js script has no document object because Node provides different host APIs.

Q: Which method is safest for setting text from user input?
- element.innerHTML = userInput
- element.textContent = userInput
correct: 1
explain: textContent treats the value as plain text and won't execute embedded HTML. innerHTML parses the value as HTML, which is an XSS vector when the content comes from untrusted input.

Q: Why does a single click listener on a parent <ul> catch clicks on its <li> children?
- Event bubbling — the event travels from the target up through ancestors to the root
- The listener is automatically copied to each child
correct: 0
explain: Events bubble from the target element up the DOM tree. A listener on the parent fires during the bubbling phase, with event.target pointing at the actual clicked child. This is event delegation.

Q: In an event handler, event.target vs event.currentTarget — what's the difference?
- target is the actual element that triggered the event; currentTarget is the element the listener is attached to
- they are always the same element
correct: 0
explain: target is the deepest element the event happened on (e.g., a child inside a button). currentTarget is the element that the listener was registered on. With event delegation they differ.

Q: When building a React app, you mostly…
- declare state and let the framework own the DOM tree; reach for raw DOM APIs only for escapes (measurements, third-party widgets, non-framework code)
- manipulate the DOM directly for every interaction
correct: 0
explain: Modern frameworks own the DOM and update it from declared state. Raw DOM API calls are an escape hatch — useful when integrating non-framework code or doing something the framework can't express, but not the default.
```
