Polyglot
Languages Web (HTML / CSS / JS) dom
Web (HTML / CSS / JS) § dom

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 requestAnimationFrame for substantial animations.
  • Use event delegation — one listener over many.
  • Use IntersectionObserver / ResizeObserver over scroll/resize listeners.
  • Use transform and opacity in CSS for substantial GPU-accelerated animation.

A note on the conventional discipline

The contemporary DOM advice:

  • Use querySelector/querySelectorAll over the older getElement*By*.
  • Use classList over className.
  • Use dataset over getAttribute("data-*").
  • Use modern methods (append, prepend, before, after, replaceWith, remove) over legacy.
  • Use closest for ancestor lookup.
  • Use textContent over innerHTML for user-supplied data.
  • Use <template> for substantial cloneable HTML.
  • Use IntersectionObserver / ResizeObserver / MutationObserver over 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.