Last 30 Days
No notifications
The Document Object Model (DOM) is a tree representation of your HTML. JavaScript can read and modify any part of this tree — changing text, styles, attributes, and structure in real time.
| Method | Returns | Example |
getElementById() | Single element | document.getElementById("app") |
querySelector() | First match | document.querySelector(".card") |
querySelectorAll() | NodeList | document.querySelectorAll("li") |
getElementsByClassName() | Live HTMLCollection | document.getElementsByClassName("btn") |
Tip: Prefer querySelector / querySelectorAll — they accept any CSS selector.
element.textContent = "Safe text"; // plain text (XSS-safe)
element.innerHTML = "<b>Bold</b>"; // parses HTML (caution!)
element.setAttribute("href", "/home");
element.classList.add("active");
element.style.color = "blue"; // inline styleconst li = document.createElement("li");
li.textContent = "New item";
ul.appendChild(li); // add to end
ul.prepend(li); // add to start
ul.removeChild(li); // remove
li.remove(); // modern removalparentNode / parentElement
children / childNodes
firstElementChild / lastElementChild
nextElementSibling / previousElementSibling
closest(".selector") ← walks up the treedocument
└── html
├── head
│ └── title
└── body
├── header
├── main
│ ├── section
│ └── article
└── footerWhen the browser loads your HTML, it builds a tree of objects in memory called the DOM (Document Object Model). Each tag becomes a *node*, attributes become properties, and JavaScript can read or change any of it on the fly.
So when you "manipulate the DOM" you're just editing that in-memory tree, and the browser repaints the page to match.
document
└── html
├── head
│ └── title "My App"
└── body
├── h1 "Welcome"
└── ul
├── li "One"
└── li "Two"Modern selector methods cover 99% of needs:
document.querySelector("#hero"); // first match by CSS selector
document.querySelector(".card");
document.querySelectorAll("li"); // all matches (a NodeList)
document.getElementById("hero"); // by id (slightly faster)Tip: querySelector accepts any CSS selector — "nav > a:not(.active)" works fine.
const title = document.querySelector("h1");title.textContent = "Hi there"; // change visible text
title.classList.add("highlight"); // add a class
title.classList.remove("dim");
title.classList.toggle("open");
title.setAttribute("data-id", "42");
title.style.color = "tomato"; // inline style (use sparingly)
textContent vs innerHTML| Property | What it does | When to use |
textContent | Plain text, escaped automatically | Default choice — XSS safe |
innerHTML | Parses HTML markup | Only with trusted content |
innerText | Like textContent but respects CSS visibility | Rare cases |
Treat innerHTML like a loaded gun. Setting element.innerHTML = userInput can let an attacker inject tags.
const li = document.createElement("li");
li.textContent = "New item";
li.classList.add("todo");const ul = document.querySelector("ul");
ul.appendChild(li); // append at end
ul.prepend(li); // insert at start
ul.insertBefore(li, ul.firstChild);
Modern shortcuts that take strings *or* nodes:
ul.append("Hello", li); // mix text + nodes
li.before(otherEl);
li.after(otherEl);
li.replaceWith(otherEl);
li.remove();el.parentElement
el.children // element children (no text nodes)
el.firstElementChild
el.lastElementChild
el.nextElementSibling
el.previousElementSiblingclosest(selector) walks up until it finds a match — perfect for event delegation:
e.target.closest(".card");const input = document.querySelector("input[name=email]");
input.value; // read
input.value = "a@b.c"; // write
input.focus();
input.disabled = true;For checkboxes and radios use .checked. For use .value (or .selectedOptions).
1. Running selectors before the DOM exists. Either put your at the end of or wrap code in document.addEventListener("DOMContentLoaded", ...).
2. Using innerHTML with user input. Use textContent or sanitise.
3. Forgetting that querySelectorAll returns a NodeList, not an array. Use forEach directly, or Array.from(list).
4. Editing styles via element.style for everything. Prefer toggling classes — your CSS file stays the source of truth.
5. Setting many properties one by one in a tight loop — see the layout-thrashing section.
Every visual change passes through a pipeline:
1. Parse HTML → DOM
2. Parse CSS → CSSOM
3. Combine → Render tree
4. Layout (where + how big)
5. Paint (fill the pixels)
6. Composite (stack layers, send to GPU)The cost depends on which property you change:
| Change | Re-runs |
width, height, margin, add/remove element | Layout → Paint → Composite (expensive) |
color, background, box-shadow | Paint → Composite |
transform, opacity | Composite only — cheapest |
This is why CSS animations using transform and opacity are silky smooth, while animating width or top jutters.
Reading a layout property (offsetHeight, getBoundingClientRect) after writing a style forces the browser to recalculate layout *now*. Doing it in a loop is brutal:
// ❌ Forced reflow on every iteration
elements.forEach((el) => {
const h = el.offsetHeight; // read
el.style.height = h + 10 + "px"; // write
});// ✅ Batch reads, then writes
const heights = elements.map((el) => el.offsetHeight);
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + "px";
});
Inserting nodes one at a time triggers reflow each time. Build them off-screen first:
const frag = document.createDocumentFragment();
for (const text of items) {
const li = document.createElement("li");
li.textContent = text;
frag.appendChild(li);
}
ul.appendChild(frag); // exactly one reflowInstead of stuffing IDs into class names:
<button data-id="42" data-action="delete">×</button>
btn.dataset.id; // "42"
btn.dataset.action; // "delete"Reads and writes any data-* attribute — perfect for event delegation.
Reusable markup without strings:
<template id="card-tmpl">
<article class="card"><h3></h3><p></p></article>
</template>
const tmpl = document.getElementById("card-tmpl");
const node = tmpl.content.cloneNode(true);
node.querySelector("h3").textContent = "Title";
container.appendChild(node);Faster and safer than building HTML strings.
MutationObserverWatch the DOM for changes (e.g. third-party scripts injecting elements):
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
console.log(m.addedNodes);
}
});
obs.observe(document.body, { childList: true, subtree: true });IntersectionObserver for Lazy WorkRun code only when an element scrolls into view (lazy-load images, infinite scroll, animate-on-scroll):
const io = new IntersectionObserver((entries) => {
for (const e of entries) {
if (e.isIntersecting) {
e.target.src = e.target.dataset.src;
io.unobserve(e.target);
}
}
});
document.querySelectorAll("img[data-src]").forEach((img) => io.observe(img));Way cheaper than listening to scroll.
ResizeObserverLike media queries but per-element — fire whenever a container's size changes. Pairs perfectly with container queries.
requestAnimationFrameFor smooth animations driven by JS, sync with the browser's paint cycle:
function tick() {
el.style.transform = translateX(${x}px);
x += 1;
if (x < 300) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);setTimeout and setInterval are NOT painted-aligned and will judder.
Encapsulate a chunk of DOM + CSS so styles can't leak in or out:
class MyCard extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: "open" });
shadow.innerHTML = <style>p{color:red}</style><p>Hi</p>;
}
}
customElements.define("my-card", MyCard);The tag is now a reusable, isolated component — no framework required.
1. Build a todo list with createElement and appendChild (no innerHTML).
2. Replace a 200-item insertion loop with a DocumentFragment and time the difference.
3. Lazy-load images on a long page using IntersectionObserver.
4. Animate an element across the screen using requestAnimationFrame *and* using transform: translateX — compare the smoothness vs animating left.