frontend / browser internals / 03_dom_bom_cssom.md

DOM, BOM, CSSOM — The Trees

7 min read source

DOM, BOM, CSSOM — The Trees

TL;DR

The browser exposes three object models:

  • DOM (Document Object Model) — the HTML element tree (document, Node, Element, HTMLElement…).
  • BOM (Browser Object Model) — everything else the browser exposes (window, navigator, location, history, screen).
  • CSSOM (CSS Object Model) — stylesheets parsed into JS objects (document.styleSheets, CSSRule, computed styles).

Together they’re “what JS can touch in a page.” Senior fluency means knowing which API lives where, what’s available in workers (which strip DOM/BOM), and the cross-realm and same-origin pitfalls.

DOM Q&A

Q: Node vs Element vs HTMLElement.

A: A class hierarchy:

Node
├── Document
├── Element
│   ├── HTMLElement
│   │   ├── HTMLDivElement
│   │   ├── HTMLInputElement
│   │   └── ...
│   └── SVGElement
├── Attr
├── CharacterData
│   ├── Text
│   └── Comment
└── DocumentFragment
  • Node — the base; everything in the DOM tree.
  • Element — an XML/HTML element with attributes (no text nodes).
  • HTMLElementElement subclass with HTML-specific properties (style, dataset, innerText).
  • HTMLInputElement/HTMLDivElement/etc. — per-tag subclasses with element-specific properties.

instanceof HTMLElement checks the entire subclass tree. Useful when narrowing Element | null to “an HTML element with a style property.”

Q: document vs window — different things?

A: document is the DOM root (the HTML tree); window is the global object + Browser Object Model. document is a property of window.

window.document === document   // true (in browsers, window's properties are global)
window.location                 // same as `location`
window.localStorage             // same as `localStorage`

In workers, window doesn’t exist; self is the global object equivalent (no DOM, no document).

Q: Query selectors — getElementById, querySelector, getElementsByTagName.

A:

Returns Live or static? Notes
getElementById(id) Element | null static fastest
querySelector(selector) Element | null static first match
querySelectorAll(selector) NodeList static not live; iterate with forEach
getElementsByTagName(tag) HTMLCollection live updates as DOM changes
getElementsByClassName(cls) HTMLCollection live same

Live vs static:

const list = document.getElementsByTagName("li");   // live
console.log(list.length);   // 3
document.body.appendChild(document.createElement("li"));
console.log(list.length);   // 4 — live updated

const list2 = document.querySelectorAll("li");      // static
console.log(list2.length);   // 3
document.body.appendChild(document.createElement("li"));
console.log(list2.length);   // 3 — snapshot

Live collections used to be a perf trap (iterating one re-checks the DOM each iteration). Modern engines optimize; rarely matters.

Q: innerHTML vs textContent vs innerText.

A:

What it sets Triggers layout? Safe?
innerHTML parsed as HTML yes (re-build subtree) XSS risk — never with user input
textContent raw text (no parsing) no safe
innerText rendered text (whitespace + style aware) yes (forces layout) safe
el.innerHTML = "<script>alert(1)</script>";   // dangerous if user input
el.textContent = "<script>";                   // literal string "<script>"
el.innerText = "Multi  spaces\n\nlines";       // collapsed per CSS

textContent is the right default for setting text. innerText is for reading what the user sees (respects display: none, line breaks via <br>, etc.).

Q: Modern DOM mutation APIs.

A: Beyond appendChild:

el.append(child1, child2, "text");        // multiple, including strings
el.prepend(child);                         // insert at start
el.before(sibling);                        // insert before this element
el.after(sibling);                          // insert after
el.replaceWith(newEl);                     // swap this element
el.remove();                                // remove this element from parent

// Older equivalents you may still see:
parent.insertBefore(child, ref);
el.parentNode.removeChild(el);

append/prepend/before/after/replaceWith accept multiple nodes and strings. Cleaner than the older boilerplate.

Q: Templates — <template> and DocumentFragment.

A:

<template id="row">
  <li class="item"><span class="name"></span></li>
</template>
const tmpl = document.getElementById("row") as HTMLTemplateElement;

function makeRow(name: string) {
  const clone = tmpl.content.cloneNode(true) as DocumentFragment;
  clone.querySelector(".name")!.textContent = name;
  return clone;
}

for (const name of names) {
  list.append(makeRow(name));    // batched insert
}

<template> is inert markup — not rendered, not run (<script> inside doesn’t execute). cloneNode(true) does a deep copy. Append the fragment, not the contents — fragments unwrap when appended.

Q: Custom elements / Web Components.

A:

class MyCounter extends HTMLElement {
  static observedAttributes = ["count"];
  #shadow = this.attachShadow({ mode: "open" });

  connectedCallback() { this.render(); }
  attributeChangedCallback(name, oldVal, newVal) { this.render(); }

  render() {
    this.#shadow.innerHTML = `<button>${this.getAttribute("count") ?? 0}</button>`;
  }
}
customElements.define("my-counter", MyCounter);
<my-counter count="5"></my-counter>

A native class extending HTMLElement. Lifecycle hooks (connectedCallback, disconnectedCallback, attributeChangedCallback). Shadow DOM gives style + DOM encapsulation.

Used in design systems shipping to mixed-framework environments (Shoelace, Lit, Material Web Components). Less common inside a single-framework app.

BOM Q&A

Q: What’s in the BOM?

A:

API What
window the global object + window-level methods (alert, setTimeout, etc.)
navigator browser info (userAgent, language, clipboard, serviceWorker, geolocation)
location current URL (href, pathname, search, hash)
history session history (pushState, replaceState, back)
screen display info (width, height, pixelDepth)
localStorage/sessionStorage persistent storage (see 04_storage_apis.md)
crypto Web Crypto API (random, hash, sign/encrypt)

Q: location and history navigation.

A:

// Read
location.href;                  // full URL
location.pathname;              // "/users/1"
location.search;                // "?tab=settings"
location.hash;                  // "#section1"

// Navigate
location.href = "/somewhere";   // full page load
location.assign("/somewhere");  // same
location.replace("/somewhere"); // same but no history entry
location.reload();

// SPA routing without reload
history.pushState({ x: 1 }, "", "/users/2");    // updates URL, no reload
history.replaceState({ x: 2 }, "", "/users/3"); // replaces current entry
window.addEventListener("popstate", (e) => {
  // user clicked back/forward; e.state is whatever you pushed
});

pushState is the foundation of SPA routing libraries (React Router, Vue Router). New Navigation API (Chrome 102+) is the modern replacement but adoption is uneven; libraries still use pushState / popstate.

Q: URL and URLSearchParams.

A: Built-in URL parsing — never roll your own:

const url = new URL("https://example.com/users/1?tab=settings&filter=active#bio");
url.protocol;     // "https:"
url.hostname;     // "example.com"
url.pathname;     // "/users/1"
url.searchParams.get("tab");        // "settings"
url.searchParams.append("page", "2");
url.toString();   // updated

// Search params alone
const params = new URLSearchParams(location.search);
for (const [k, v] of params) console.log(k, v);

Use these for any URL manipulation. Replaces hand-rolled regex parsing.

CSSOM Q&A

Q: How do you read computed styles?

A: getComputedStyle(element):

const styles = getComputedStyle(el);
styles.color;            // "rgb(0, 0, 0)" — always resolved (no "currentColor", no shortcuts)
styles.width;             // "300px" — resolved to px
styles.getPropertyValue("--my-var");   // CSS custom property

getComputedStyle is expensive — forces layout if you read a dimension. Cache the result; don’t call in tight loops.

Q: Setting CSS from JS.

A:

// Inline styles
el.style.color = "red";
el.style.setProperty("--brand-color", "#3b82f6");
el.style.transform = "translateX(10px)";

// Multiple at once
Object.assign(el.style, { color: "red", fontSize: "16px" });

// Toggle CSS classes
el.classList.add("active");
el.classList.remove("hidden");
el.classList.toggle("expanded");
el.classList.contains("expanded");

// Replace inline styles entirely
el.style.cssText = "color: red; font-size: 16px";

Prefer classList over inline styles when possible — CSS classes are cacheable, themeable, and don’t compete with stylesheets.

Q: document.styleSheets and dynamic rules.

A: Programmatic access to loaded stylesheets:

const sheet = document.styleSheets[0];
sheet.insertRule(".dynamic { color: blue; }", sheet.cssRules.length);
sheet.deleteRule(0);

Used by CSS-in-JS libraries to inject rules at runtime. Rarely used directly in app code.

Q: CSSStyleSheet constructor (constructable stylesheets).

A: Modern API — build a stylesheet object in JS, share across documents:

const sheet = new CSSStyleSheet();
sheet.replaceSync(".dynamic { color: blue; }");

// Attach to a shadow root or document
shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, sheet];
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];

Shared, deduplicatable. Used by web component libraries for style isolation without runtime cost.

Gotchas / edge cases

  • Workers have no DOMdocument, window, localStorage, alert all unavailable. Use postMessage for communication.
  • alert, confirm, prompt block the event loop — synchronously freeze the page until dismissed. Never use in production UI.
  • document.write after page load wipes the document — historical footgun. Don’t use; ever.
  • Live HTMLCollections during iteration can cause “forever loop” bugs — for (let i=0; i<list.length; i++) parent.appendChild(list[i]) is undefined behavior. Snapshot with Array.from(list).
  • querySelector is slower than getElementById in micro-benchmarks; matters only at extreme scale.
  • DOM mutations are sync, paint is async — modifying then reading offsetWidth triggers a synchronous layout but paint hasn’t happened yet (the user hasn’t seen the change).
  • Same-origin policy restricts cross-frame DOM access — an <iframe> from a different origin’s contentDocument throws.

What a senior is expected to say

  • “DOM is the HTML tree; BOM is everything else the browser exposes (window, navigator, location, history, screen); CSSOM is parsed stylesheets. Workers strip DOM/BOM.”
  • textContent for setting text (safe); innerHTML only with trusted input; innerText for reading rendered text (forces layout).”
  • “Use URL and URLSearchParams for URL parsing — never regex. SPA routing built on history.pushState + popstate.”
  • getComputedStyle forces layout if you read a dimension — cache the result, don’t call in loops.”
  • “Custom elements give native components in mixed-framework environments; shadow DOM gives style encapsulation.”
  • “Constructable CSSStyleSheet + adoptedStyleSheets is the modern way to share styles across roots without duplication.”

Cross-references

Further reading