What is the DOM?
The Document Object Model (DOM) is the browser's in-memory representation of an HTML page as a tree of nodes. JavaScript interacts with the page by reading and modifying this tree.
<!-- This HTML... -->
<html>
<body>
<h1 id="title">Hello</h1>
<ul class="list">
<li>Item 1</li>
<li>Item 2</li>
</ul>
</body>
</html>
<!-- ...becomes a tree of nodes:
document
html
body
h1 #title
"Hello" (text node)
ul .list
li
"Item 1" (text node)
li
"Item 2" (text node)
-->
// document is the entry point to the DOM
console.log(document.title); // "My Page"
console.log(document.URL); // current URL
console.log(document.body); // <body> element
console.log(document.head); // <head> element
console.log(document.documentElement); // <html> element
Selecting Elements
querySelector and querySelectorAll accept any CSS selector and
are the modern standard for element selection.
// querySelector - returns FIRST matching element (or null)
const title = document.querySelector("#title"); // by id
const btn = document.querySelector(".btn"); // by class
const input = document.querySelector("input[type='email']"); // by attribute
const first = document.querySelector("ul > li"); // CSS selector
const heading = document.querySelector("h1, h2, h3"); // first of any heading
// querySelectorAll - returns ALL matching elements (NodeList)
const items = document.querySelectorAll(".list li");
const buttons = document.querySelectorAll("button");
// Convert NodeList to Array to use array methods
const itemsArr = Array.from(items);
const texts = [...items].map(el => el.textContent);
// Legacy selectors (still useful)
const byId = document.getElementById("title"); // returns Element or null
const byClass = document.getElementsByClassName("btn"); // live HTMLCollection
const byTag = document.getElementsByTagName("p"); // live HTMLCollection
// Scoped selection: search within a specific element
const nav = document.querySelector("nav");
const link = nav.querySelector("a.active"); // search inside nav only
querySelector returns null if no match is found. Calling a method on null throws a TypeError. Always check: const el = document.querySelector("#id"); if (el) { el.textContent = "..."; } or use optional chaining: el?.textContent.
DOM Traversal
Navigate the DOM tree using parent, child, and sibling relationships:
const list = document.querySelector("ul");
// Children (Element-only, ignores text nodes)
console.log(list.children); // HTMLCollection of li elements
console.log(list.firstElementChild); // first li
console.log(list.lastElementChild); // last li
console.log(list.children.length); // number of child elements
// Parent
const item = document.querySelector("li");
console.log(item.parentElement); // the ul
console.log(item.parentElement.parentElement); // further up
// Siblings
const firstItem = list.firstElementChild;
console.log(firstItem.nextElementSibling); // next li
console.log(firstItem.previousElementSibling); // null (it is the first)
// closest() - find nearest ancestor matching selector (including self)
const btn = document.querySelector("button");
const form = btn.closest("form"); // walks up until finding a form
const card = btn.closest(".card"); // nearest .card ancestor
// matches() - test if element matches a selector
console.log(item.matches("li.active")); // true/false
console.log(item.matches(":first-child")); // true/false
Creating Elements
// Create element
const newItem = document.createElement("li");
newItem.textContent = "New Item";
newItem.className = "list-item";
// Add to DOM
const list = document.querySelector("ul");
list.appendChild(newItem); // add at end
list.prepend(newItem); // add at start
list.insertBefore(newItem, firstLi); // before specific element
// Modern: insertAdjacentElement
// "beforebegin" | "afterbegin" | "beforeend" | "afterend"
list.insertAdjacentElement("afterend", newItem); // after the list
// Create multiple elements efficiently
const fragment = document.createDocumentFragment();
["Apple", "Banana", "Cherry"].forEach(name => {
const li = document.createElement("li");
li.textContent = name;
fragment.appendChild(li); // append to fragment (no reflow yet)
});
list.appendChild(fragment); // one DOM update
// innerHTML for simple trusted HTML insertion
list.innerHTML += "<li>New Item</li>"; // simple but re-parses all children
list.insertAdjacentHTML("beforeend", "<li>New Item</li>"); // more efficient
textContent vs innerHTML
const el = document.querySelector("#output");
// textContent - plain text, no HTML parsing
el.textContent = "Hello <b>World</b>";
// Renders literally: Hello <b>World</b> (the tags are text)
// Safe for user input (prevents XSS!)
const userInput = '<script>alert("xss")</script>';
el.textContent = userInput; // safe - displays as text
// innerHTML - parses HTML markup
el.innerHTML = "Hello <b>World</b>";
// Renders: Hello World (bold)
// DANGER: never use innerHTML with user input
// el.innerHTML = userInput; // XSS vulnerability!
// innerText vs textContent
// textContent: raw text including hidden elements, no layout considered
// innerText: visible text only, considers CSS (getComputedStyle) - slower
el.innerText = "Hello"; // respects CSS, triggers layout
// outerHTML - includes the element itself
console.log(el.outerHTML); // <div id="output">Hello</div>
// Remove all children
el.textContent = ""; // fastest way to clear content
// el.innerHTML = ""; // also works but slower
Attributes and Classes
const btn = document.querySelector("button");
// HTML attributes
btn.setAttribute("disabled", ""); // add attribute
btn.getAttribute("type"); // read: "button"
btn.removeAttribute("disabled"); // remove
btn.hasAttribute("disabled"); // false
// Properties (faster than getAttribute for standard attributes)
btn.disabled = true; // same as setAttribute("disabled", "")
btn.id = "submit-btn";
btn.type = "submit";
// Dataset - custom data-* attributes
// <div data-user-id="42" data-role="admin">
const div = document.querySelector("[data-user-id]");
console.log(div.dataset.userId); // "42" (camelCase access)
console.log(div.dataset.role); // "admin"
div.dataset.newProp = "value"; // adds data-new-prop="value"
// classList - manage CSS classes
const card = document.querySelector(".card");
card.classList.add("active", "highlight"); // add multiple
card.classList.remove("hidden"); // remove
card.classList.toggle("open"); // add if absent, remove if present
card.classList.toggle("dark", isDark); // add/remove based on condition
card.classList.contains("active"); // true/false
card.classList.replace("old-class", "new-class"); // swap
console.log(card.className); // all classes as string
Inline Styles
const box = document.querySelector(".box");
// Set inline styles (camelCase property names)
box.style.backgroundColor = "blue"; // background-color
box.style.fontSize = "18px"; // font-size
box.style.marginTop = "20px"; // margin-top
box.style.transform = "rotate(45deg)";
// Remove inline style
box.style.backgroundColor = ""; // set to empty string removes it
// Read computed style (includes CSS from stylesheets)
const styles = getComputedStyle(box);
console.log(styles.width); // "300px" (actual computed value)
console.log(styles.backgroundColor); // "rgb(0, 0, 255)"
// Better approach: toggle classes instead of direct style manipulation
// Keeps styling in CSS, JS only manages state
box.classList.add("box-large"); // define .box-large in CSS
box.classList.remove("box-small");