DOM
The Document Object Model (DOM) is the principal API for reading and manipulating HTML and CSS from JavaScript. Every HTML element is a Node in a tree; the document is the root entry point. The principal operations: selection (document.querySelector, getElementById), traversal (parentNode, children, nextElementSibling), modification (textContent, innerHTML, setAttribute, classList), creation (document.createElement), insertion (append, prepend, insertBefore), removal (remove). The combination — substantial selectors, the substantial mutation surface, the substantial style and class manipulation, the conventional contemporary methods (closest, matches, classList, dataset) — is the substance of DOM work.
Selecting elements
// By ID:
const el = document.getElementById("header"); // single element or null
// By selector (most conventional):
const el = document.querySelector(".item"); // first match
const all = document.querySelectorAll(".item"); // NodeList of all
// By tag:
const headings = document.getElementsByTagName("h1"); // live HTMLCollection
// By class:
const items = document.getElementsByClassName("item"); // live HTMLCollection
// Within an element:
const child = parent.querySelector(".child");
The querySelector and querySelectorAll admit any CSS selector — substantial and conventional. The getElementsBy* methods return live collections (update as DOM changes); querySelectorAll returns a static NodeList.
// CSS selectors of substantial form:
document.querySelector("#main");
document.querySelector(".item.active");
document.querySelector("input[type='checkbox']:checked");
document.querySelector("nav > ul > li:first-child");
document.querySelectorAll("p.warning, p.error");
document.querySelectorAll("article:has(h2.featured)"); // :has() (modern)
Iterating NodeList
const items = document.querySelectorAll(".item");
// forEach (NodeList admits this):
items.forEach(el => console.log(el.textContent));
// for...of:
for (const el of items) {
console.log(el.textContent);
}
// To array (admit substantial array methods):
const arr = [...items];
const arr2 = Array.from(items);
Traversal
const el = document.querySelector(".item");
el.parentNode; // any parent (incl. document)
el.parentElement; // parent element only
el.children; // child elements (HTMLCollection)
el.childNodes; // all child nodes (incl. text, comment)
el.firstElementChild;
el.lastElementChild;
el.nextElementSibling;
el.previousElementSibling;
// Closest ancestor matching selector:
el.closest(".container"); // self or ancestor
// Matches selector?
el.matches(".item.active"); // boolean
// Contains?
parent.contains(child); // boolean
Reading and writing content
const el = document.querySelector(".message");
// Text content (plain text only — no HTML):
el.textContent;
el.textContent = "New message";
// HTML content (with substantial XSS risk):
el.innerHTML;
el.innerHTML = "<strong>Bold</strong>"; // DANGER if input is user-supplied
// Outer HTML (incl. self):
el.outerHTML;
// innerText (visible text — respects styling, slower):
el.innerText;
The conventional discipline — prefer textContent over innerHTML for user-supplied data; innerHTML admits substantial XSS risk if the source is not trusted.
For substantial safe HTML insertion:
// Use insertAdjacentHTML for substantial position:
el.insertAdjacentHTML("beforebegin", html);
el.insertAdjacentHTML("afterbegin", html);
el.insertAdjacentHTML("beforeend", html);
el.insertAdjacentHTML("afterend", html);
// Or build with createElement (safe):
const span = document.createElement("span");
span.textContent = userInput;
span.classList.add("user-content");
el.appendChild(span);
Attributes
const el = document.querySelector("input");
// Reading:
el.getAttribute("type"); // "text"
el.hasAttribute("disabled");
el.id; // direct property (most common attrs)
el.className; // legacy; use classList
el.disabled; // boolean attrs as direct property
// Writing:
el.setAttribute("data-id", "123");
el.removeAttribute("disabled");
el.id = "new-id";
el.disabled = true; // for boolean attrs
// data-* attributes via dataset:
el.dataset.userId; // <el data-user-id="42"> → "42"
el.dataset.userId = "99"; // sets data-user-id="99"
// Custom attributes:
el.setAttribute("aria-label", "Close dialog");
el.getAttribute("aria-label");
The principal distinction: attributes are HTML serialised form; properties are JavaScript object access. For most attributes, the property mirrors. For boolean attributes (checked, disabled, readonly), the property is current state; the attribute is initial value.
Classes
const el = document.querySelector(".item");
el.classList.add("active");
el.classList.add("active", "selected"); // multiple
el.classList.remove("active");
el.classList.toggle("active");
el.classList.toggle("active", true); // forced add
el.classList.toggle("active", false); // forced remove
el.classList.contains("active"); // boolean
el.classList.replace("old", "new");
// Iteration:
for (const cls of el.classList) {
console.log(cls);
}
// Get all classes:
const classes = [...el.classList];
The classList is the conventional contemporary way — admits substantial cleaner than the legacy className string manipulation.
Styles
const el = document.querySelector(".box");
// Inline style:
el.style.color = "red";
el.style.fontSize = "16px"; // camelCase for hyphenated
el.style.backgroundColor = "yellow";
el.style.cssText = "color: red; font-size: 16px;"; // bulk
// Custom properties (CSS variables):
el.style.setProperty("--theme", "dark");
el.style.getPropertyValue("--theme");
// Computed style (final, including stylesheet rules):
const styles = getComputedStyle(el);
styles.color;
styles.fontSize;
styles.getPropertyValue("--theme");
The conventional discipline — prefer class manipulation (classList) for substantial style changes; reserve direct style for substantial dynamic values.
Creating elements
const div = document.createElement("div");
div.id = "main";
div.classList.add("container");
div.textContent = "Hello";
// Or with substantial more setup:
const button = document.createElement("button");
button.type = "button";
button.classList.add("btn", "btn-primary");
button.textContent = "Click me";
button.dataset.action = "submit";
button.setAttribute("aria-label", "Submit form");
button.addEventListener("click", handleClick);
// Document fragment (for substantial batch insert):
const frag = document.createDocumentFragment();
for (const item of items) {
const li = document.createElement("li");
li.textContent = item.name;
frag.appendChild(li);
}
list.appendChild(frag); // single reflow
The DocumentFragment admits substantial performance for substantial batch insertion — single reflow.
Inserting and removing
const parent = document.querySelector(".list");
const child = document.createElement("li");
// Modern methods:
parent.append(child, child2); // at end (multiple OK)
parent.prepend(child); // at start
parent.append("text node directly"); // text OK
// Sibling insertion:
existingChild.before(newChild);
existingChild.after(newChild);
// Replace:
existingChild.replaceWith(newChild);
// Remove:
existingChild.remove();
// Legacy (still works):
parent.appendChild(child);
parent.insertBefore(child, referenceChild);
parent.removeChild(existingChild);
parent.replaceChild(newChild, oldChild);
The modern append, prepend, before, after, replaceWith, remove admit substantial cleaner code — admit multiple arguments and admit text directly.
Cloning
const el = document.querySelector(".template");
const shallow = el.cloneNode(false); // self only
const deep = el.cloneNode(true); // including children
// importNode — admits substantial cross-document:
const imported = document.importNode(otherDoc.querySelector("..."), true);
Forms
const form = document.querySelector("form");
const input = document.querySelector("input[name='email']");
// Reading:
input.value;
input.checked; // for checkbox/radio
input.files; // for file inputs
// Writing:
input.value = "alice@example.com";
// Form submission:
form.addEventListener("submit", (e) => {
e.preventDefault(); // prevent default submission
const data = new FormData(form);
const obj = Object.fromEntries(data);
console.log(obj);
});
// Programmatic submit:
form.submit();
form.requestSubmit(); // triggers validation/submit event
Treated more substantially in the Forms and inputs page.
Templates
The <template> element admits substantial cloneable content:
<template id="card-template">
<div class="card">
<h2 class="title"></h2>
<p class="body"></p>
</div>
</template>
const tmpl = document.getElementById("card-template");
const clone = tmpl.content.cloneNode(true);
clone.querySelector(".title").textContent = "Hello";
clone.querySelector(".body").textContent = "World";
document.body.appendChild(clone);
Shadow DOM
For substantial encapsulation (Web Components):
const host = document.querySelector("#widget");
const shadow = host.attachShadow({ mode: "open" });
shadow.innerHTML = `
<style>p { color: red; }</style>
<p>Inside the shadow</p>
`;
// Querying inside shadow:
shadow.querySelector("p");
// From outside:
host.shadowRoot; // open mode
The Shadow DOM admits substantial CSS isolation and substantial component-style encapsulation.
DOM dimensions and position
const el = document.querySelector(".box");
// Size:
el.offsetWidth; // including border, padding
el.offsetHeight;
el.clientWidth; // excluding border, scrollbar
el.clientHeight;
el.scrollWidth; // including overflow
el.scrollHeight;
// Position relative to offsetParent:
el.offsetTop;
el.offsetLeft;
// Absolute position (relative to viewport):
const rect = el.getBoundingClientRect();
rect.top;
rect.left;
rect.right;
rect.bottom;
rect.width;
rect.height;
rect.x;
rect.y;
// Scroll position:
el.scrollTop;
el.scrollLeft;
window.scrollX;
window.scrollY;
// Document scroll (older):
document.documentElement.scrollTop;
Scrolling
// Scroll to element:
el.scrollIntoView();
el.scrollIntoView({ behavior: "smooth", block: "center" });
// Scroll window:
window.scrollTo(0, 0);
window.scrollTo({ top: 0, behavior: "smooth" });
// Scroll element:
el.scrollTo(0, 100);
el.scrollBy(0, 100); // relative
Common patterns
Render a list
function renderList(items) {
const list = document.querySelector("#list");
list.innerHTML = ""; // clear
const frag = document.createDocumentFragment();
for (const item of items) {
const li = document.createElement("li");
li.textContent = item.name;
li.dataset.id = item.id;
frag.appendChild(li);
}
list.appendChild(frag);
}
Toggle visibility
const el = document.querySelector(".panel");
el.hidden = true; // sets hidden attribute
el.hidden = false;
// Or via class (admits substantial CSS control):
el.classList.toggle("hidden");
Delegated event listener
document.querySelector(".list").addEventListener("click", (e) => {
const item = e.target.closest(".item");
if (!item) return; // not on an item
const id = item.dataset.id;
handleItemClick(id);
});
The pattern admits substantial efficiency — one listener for substantial dynamically-added items.
Conditional class
el.classList.toggle("active", condition);
Fade in (with CSS)
.panel {
opacity: 0;
transition: opacity 200ms;
}
.panel.visible {
opacity: 1;
}
panel.classList.add("visible");
Read query params
const params = new URLSearchParams(window.location.search);
const id = params.get("id");
const allIds = params.getAll("id"); // multiple
Update URL
const url = new URL(window.location);
url.searchParams.set("filter", "active");
window.history.pushState({}, "", url); // adds to history
window.history.replaceState({}, "", url); // no new history entry
Detect DOM ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
// Or with `defer` attribute on the script — runs after DOM ready
MutationObserver
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const node of m.addedNodes) {
console.log("added:", node);
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
characterData: true
});
// Stop:
observer.disconnect();
IntersectionObserver
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
observer.unobserve(entry.target);
}
}
}, { threshold: 0.1 });
document.querySelectorAll(".lazy").forEach(el => observer.observe(el));
The IntersectionObserver admits substantial efficient lazy-loading and substantial scroll-trigger patterns.
ResizeObserver
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
console.log(`resized: ${width}x${height}`);
}
});
observer.observe(document.querySelector(".panel"));
Working with <dialog>
const dialog = document.querySelector("dialog");
dialog.showModal(); // modal
dialog.show(); // non-modal
dialog.close();
dialog.close("confirmed"); // with returnValue
dialog.addEventListener("close", () => {
console.log(dialog.returnValue);
});
Detecting reduced motion
const prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!prefersReduced) {
runAnimation();
}
// Reactively:
window.matchMedia("(prefers-reduced-motion: reduce)")
.addEventListener("change", (e) => {
// ...
});
Element tagging with WeakMap
const elementData = new WeakMap();
function attach(el, data) {
elementData.set(el, data);
}
function get(el) {
return elementData.get(el);
}
The pattern admits substantial cleanup — when the element is garbage-collected, the data is too.
Parsing HTML safely
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
const items = doc.querySelectorAll(".item");
// Or for substantial fragment parsing:
const tmpl = document.createElement("template");
tmpl.innerHTML = html;
const frag = tmpl.content;
requestAnimationFrame
function animate() {
// update DOM here
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
// One-off:
requestAnimationFrame(() => {
el.classList.add("animate");
});
The mechanism admits substantial smooth animation — synchronised with the browser’s frame rate (typically 60fps).
A note on performance
The principal DOM performance considerations:
- Batch DOM mutations — use
DocumentFragment, build off-DOM, then insert. - Avoid layout thrashing — alternating reads (
offsetWidth) and writes triggers substantial reflows. - Use
requestAnimationFramefor substantial animations. - Use event delegation — one listener over many.
- Use
IntersectionObserver/ResizeObserverover scroll/resize listeners. - Use
transformandopacityin CSS for substantial GPU-accelerated animation.
A note on the conventional discipline
The contemporary DOM advice:
- Use
querySelector/querySelectorAllover the oldergetElement*By*. - Use
classListoverclassName. - Use
datasetovergetAttribute("data-*"). - Use modern methods (
append,prepend,before,after,replaceWith,remove) over legacy. - Use
closestfor ancestor lookup. - Use
textContentoverinnerHTMLfor user-supplied data. - Use
<template>for substantial cloneable HTML. - Use
IntersectionObserver/ResizeObserver/MutationObserverover polling. - Reach for a framework (React, Vue, Svelte, Solid, Lit) for substantial UIs — direct DOM manipulation does not scale to substantial state-driven UIs.
The combination — substantial selector mechanism, substantial mutation surface, substantial style and class manipulation, substantial observer APIs, substantial form integration — is the substance of DOM work. The discipline is largely about choosing modern, ergonomic methods over legacy ones; using event delegation; and reaching for frameworks when state grows.