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).HTMLElement—Elementsubclass 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 DOM —
document,window,localStorage,alertall unavailable. Use postMessage for communication. alert,confirm,promptblock the event loop — synchronously freeze the page until dismissed. Never use in production UI.document.writeafter 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 withArray.from(list). querySelectoris slower thangetElementByIdin micro-benchmarks; matters only at extreme scale.- DOM mutations are sync, paint is async — modifying then reading
offsetWidthtriggers 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’scontentDocumentthrows.
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.”
- “
textContentfor setting text (safe);innerHTMLonly with trusted input;innerTextfor reading rendered text (forces layout).” - “Use
URLandURLSearchParamsfor URL parsing — never regex. SPA routing built onhistory.pushState+popstate.” - “
getComputedStyleforces 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+adoptedStyleSheetsis the modern way to share styles across roots without duplication.”
Cross-references
- Event loop (DOM mutations are sync, rAF before paint): 01_event_loop.md
- Rendering pipeline (DOM → render tree): 02_rendering_pipeline_deep.md
- Workers (where DOM is absent): 06_web_workers.md
- Storage APIs (BOM continued): 04_storage_apis.md
Further reading
- MDN — DOM: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model
- MDN — URL API: https://developer.mozilla.org/en-US/docs/Web/API/URL_API
- MDN — Web Components: https://developer.mozilla.org/en-US/docs/Web/API/Web_components
- WHATWG DOM Standard: https://dom.spec.whatwg.org/