This is a complete, beginner-to-advanced guide on the Document Object Model (DOM) in JavaScript. You will learn how to select, create, modify, delete, traverse, and respond to events on HTML elements β all using vanilla JavaScript.
π Table of Contents
- What is the DOM?
- The DOM Tree Structure
- Accessing the DOM β The document Object
- Selecting Elements
- Manipulating Content
- Manipulating Styles and Classes
- Manipulating Attributes
- Creating and Inserting Elements
- Removing and Replacing Elements
- DOM Events
- Event Bubbling, Capturing, and Delegation
- Traversing the DOM
- Element Dimensions and Position
- Working with Forms
- Real-World Project β Dynamic To-Do List
- Best Practices and Performance Tips
1οΈβ£ What is the DOM?
When a browser loads an HTML page, it reads the markup and creates a live, in-memory representation of that page called the Document Object Model (DOM). The DOM is not part of JavaScript β it is a Web API built into the browser that JavaScript can access and manipulate.
Think of the DOM as a live copy of your HTML. Any change you make to the DOM is instantly reflected in the browser without reloading the page. This is what makes modern web pages interactive.
Note
What the DOM Gives You
- π Access any element on the page
- βοΈ Read or change the content, structure, and styles of elements
- π± Create, insert, and remove elements dynamically
- π±οΈ Listen and respond to user events like clicks, typing, and scrolling
- π Update the page without a full reload
2οΈβ£ The DOM Tree Structure
The DOM represents an HTML document as a hierarchical tree of nodes. Every part of the HTML β elements, text, comments, and even whitespace β becomes a node in this tree.
Node Types
| Node Type | Constant | Example |
|---|---|---|
| Element Node | Node.ELEMENT_NODE (1) | <div>, <p>, <h1> |
| Text Node | Node.TEXT_NODE (3) | The text inside an element |
| Comment Node | Node.COMMENT_NODE (8) | <!-- a comment --> |
| Document Node | Node.DOCUMENT_NODE (9) | The root document object |
Visual Tree Example
Given this HTML:
index.html
<!DOCTYPE html>
<html>
<head>
<title>My Page</title>
</head>
<body>
<h1 id="heading">Hello World</h1>
<ul>
<li class="item">Item 1</li>
<li class="item">Item 2</li>
</ul>
</body>
</html>The DOM tree looks like this:
DOM Tree
document
βββ <html>
βββ <head>
β βββ <title>
β βββ [text: "My Page"]
βββ <body>
βββ <h1 id="heading">
β βββ [text: "Hello World"]
βββ <ul>
βββ <li class="item">
β βββ [text: "Item 1"]
βββ <li class="item">
βββ [text: "Item 2"]Note
3οΈβ£ Accessing the DOM β The document Object
The document object is your main entry point to the DOM. It is a global object available in every browser environment and represents the entire web page.
document-object.js
// The document object itself
console.log(document); // The entire DOM
console.log(document.nodeType); // 9 (DOCUMENT_NODE)
console.log(document.nodeName); // "#document"
// Common document properties
console.log(document.title); // Page title from <title> tag
console.log(document.URL); // Full URL of the page
console.log(document.domain); // Domain name
console.log(document.charset); // Character encoding (e.g. "UTF-8")
console.log(document.readyState); // "loading" | "interactive" | "complete"
// Shortcut references
console.log(document.head); // The <head> element
console.log(document.body); // The <body> element
console.log(document.documentElement); // The <html> element
// All elements of a specific type
console.log(document.images); // All <img> elements
console.log(document.links); // All <a> elements
console.log(document.forms); // All <form> elementsDOMContentLoaded vs load
You should almost always wait for the DOM to be ready before running your JavaScript. There are two events for this:
dom-ready.js
// DOMContentLoaded β fires when HTML is parsed and DOM is ready
// (does NOT wait for images, stylesheets, or other resources)
document.addEventListener("DOMContentLoaded", () => {
console.log("DOM is ready!");
// Safe to query and manipulate DOM here
});
// load β fires when the ENTIRE page including all resources is loaded
window.addEventListener("load", () => {
console.log("Everything including images is loaded!");
});Note
4οΈβ£ Selecting Elements
Before you can do anything with an element, you must select it. JavaScript provides multiple methods for selecting elements from the DOM.
All Selection Methods
| Method | Selects By | Returns | Live? |
|---|---|---|---|
| getElementById(id) | id attribute | Element or null | N/A |
| getElementsByClassName(cls) | class name | HTMLCollection | β Live |
| getElementsByTagName(tag) | tag name | HTMLCollection | β Live |
| getElementsByName(name) | name attribute | NodeList | β Live |
| querySelector(selector) | First CSS match | Element or null | β Static |
| querySelectorAll(selector) | All CSS matches | NodeList | β Static |
getElementById
Selects a single element by its id attribute. The fastest selector available.
get-by-id.js
// HTML: <h1 id="main-title">Hello</h1>
const title = document.getElementById("main-title");
console.log(title); // <h1 id="main-title">Hello</h1>
console.log(title.id); // "main-title"
console.log(title.tagName); // "H1"
// Returns null if not found
const missing = document.getElementById("does-not-exist");
console.log(missing); // nullgetElementsByClassName
Returns a live HTMLCollection of all elements with the given class. "Live" means it automatically updates if elements are added or removed.
get-by-class.js
// HTML: <li class="item">...</li> (multiple)
const items = document.getElementsByClassName("item");
console.log(items); // HTMLCollection [li, li, li]
console.log(items.length); // 3
console.log(items[0]); // First <li class="item">
// Loop through the collection
for (let i = 0; i < items.length; i++) {
console.log(items[i].textContent);
}
// Or convert to an array first (safer for iteration)
const itemsArray = Array.from(items);
itemsArray.forEach(item => console.log(item.textContent));querySelector and querySelectorAll
These are the most powerful and flexible selectors β they accept any valid CSS selector string.
query-selector.js
// querySelector β returns the FIRST match
const firstItem = document.querySelector(".item");
const header = document.querySelector("header");
const submit = document.querySelector("button[type='submit']");
const heroTitle = document.querySelector("#hero .title"); // nested selectors
const oddRow = document.querySelector("tr:nth-child(odd)"); // pseudo-classes
// querySelectorAll β returns ALL matches as a static NodeList
const allItems = document.querySelectorAll(".item");
const allLinks = document.querySelectorAll("a[href]");
const headings = document.querySelectorAll("h1, h2, h3"); // multiple selectors
// NodeList supports forEach directly
allItems.forEach(item => {
console.log(item.textContent);
});
// Convert NodeList to Array for full array methods
const itemsArr = [...allItems]; // spread operator
const texts = itemsArr.map(el => el.textContent);Scoped Queries β Searching Within an Element
You can call querySelector and querySelectorAll on any element, not just on document. This scopes the search to that element's subtree.
scoped-query.js
const nav = document.querySelector("nav");
// Only searches inside the <nav> element
const navLinks = nav.querySelectorAll("a");
const activeLink = nav.querySelector(".active");
// Much more efficient than searching the whole document
const sidebar = document.querySelector(".sidebar");
const sidebarButtons = sidebar.querySelectorAll("button");Note
5οΈβ£ Manipulating Content
Once you have an element reference, you can read or change what's inside it using several properties.
innerHTML
Gets or sets the HTML markup inside an element. When setting, it parses the string as HTML.
inner-html.js
const box = document.querySelector("#box");
// GET β read the inner HTML
console.log(box.innerHTML);
// Returns: "<strong>Hello</strong> <em>World</em>"
// SET β replace inner content with new HTML
box.innerHTML = "<h2>New Title</h2><p>New paragraph.</p>";
// Append without replacing (use +=)
box.innerHTML += "<p>Added paragraph</p>";
// Clear all content
box.innerHTML = "";
// Template literals make complex HTML easy
const name = "Alice";
const score = 95;
box.innerHTML = `
<div class="result">
<h3>Player: ${name}</h3>
<p>Score: <strong>${score}</strong></p>
</div>
`;Note
textContent
Gets or sets the plain text content of an element and all its descendants. Does not parse HTML β everything is treated as literal text. This is the safe alternative to innerHTML for user input.
text-content.js
const para = document.querySelector("p");
// GET β returns all text inside, including nested elements
console.log(para.textContent);
// Returns: "Hello World" (from <p>Hello <strong>World</strong></p>)
// SET β replaces all content with plain text
para.textContent = "New content";
// HTML tags are treated as literal text β not rendered
para.textContent = "<strong>This is NOT bold</strong>";
// Displays on screen as: <strong>This is NOT bold</strong>
// Safe to use with user-provided data
const userInput = document.querySelector("#search").value;
resultEl.textContent = "Results for: " + userInput; // XSS safeinnerText
innerText is similar to textContent but is CSS-aware β it returns only visible text, respecting display:none and other CSS rules.
inner-text.js
// HTML:
// <p>Visible <span style="display:none">Hidden</span> text</p>
const para = document.querySelector("p");
console.log(para.textContent); // "Visible Hidden text" (includes hidden)
console.log(para.innerText); // "Visible text" (skips hidden)
// innerText also normalizes whitespace like the browser does
// textContent returns raw whitespace from the sourceComparison Table
| Property | Parses HTML | XSS Safe | CSS Aware | Performance |
|---|---|---|---|---|
| innerHTML | β Yes | β οΈ No | β No | Medium |
| textContent | β No | β Yes | β No | Fast |
| innerText | β No | β Yes | β Yes | Slow (forces reflow) |
outerHTML
outerHTML gets or sets the HTML of the element including the element itself β not just its contents.
outer-html.js
const btn = document.querySelector("button");
// GET β includes the element tag itself
console.log(btn.outerHTML);
// Returns: '<button id="myBtn" class="primary">Click Me</button>'
// SET β replaces the element entirely with new HTML
btn.outerHTML = '<a href="/home">Go Home</a>';
// The <button> is now gone and replaced by an <a> tag
// Note: after setting outerHTML, the original variable (btn)
// still points to the old detached node β be careful!6οΈβ£ Manipulating Styles and Classes
The style Property β Inline Styles
Every DOM element has a style property that maps to its inline CSS. CSS property names are written in camelCase in JavaScript (e.g. background-color becomes backgroundColor).
inline-styles.js
const box = document.querySelector(".box");
// Set individual CSS properties
box.style.width = "200px";
box.style.height = "100px";
box.style.backgroundColor = "#3b82f6";
box.style.color = "white";
box.style.padding = "1rem";
box.style.borderRadius = "8px";
box.style.fontSize = "18px";
box.style.display = "flex";
box.style.justifyContent = "center";
// Remove a style by setting it to empty string
box.style.backgroundColor = "";
// Set multiple styles at once using cssText
box.style.cssText = "width: 200px; height: 100px; background: red;";
// WARNING: cssText replaces ALL existing inline styles!
// Read a specific inline style
console.log(box.style.width); // "200px" (only reads inline, not from stylesheet)getComputedStyle β Reading Final Applied Styles
el.style only reads inline styles. To read the final computed value (including styles from CSS files), use window.getComputedStyle().
computed-style.js
const btn = document.querySelector("button");
// Get the fully computed style object
const computed = window.getComputedStyle(btn);
// Read any property β values come from all CSS sources
console.log(computed.backgroundColor); // "rgb(59, 130, 246)"
console.log(computed.fontSize); // "16px"
console.log(computed.display); // "inline-block"
console.log(computed.marginTop); // "8px"
// Read a pseudo-element's styles
const beforeStyle = window.getComputedStyle(btn, "::before");
console.log(beforeStyle.content); // e.g. '"β"'The classList API β Recommended Approach
Rather than setting styles inline, the best practice is to define styles in your CSS file and toggle CSS classesusing the classList API. This keeps your logic and presentation separate.
class-list.js
const card = document.querySelector(".card");
// Add one or more classes
card.classList.add("active");
card.classList.add("highlighted", "visible"); // multiple at once
// Remove a class
card.classList.remove("hidden");
card.classList.remove("hidden", "disabled"); // multiple at once
// Toggle β adds if absent, removes if present
card.classList.toggle("dark-mode");
// Toggle with force parameter
card.classList.toggle("active", true); // Force add
card.classList.toggle("active", false); // Force remove
// Replace one class with another
card.classList.replace("old-theme", "new-theme");
// Check if element has a class β returns boolean
if (card.classList.contains("active")) {
console.log("Card is active!");
}
// See all current classes
console.log(card.classList); // DOMTokenList ["card", "active"]
console.log(card.className); // "card active" (as a string)
console.log([...card.classList]); // ["card", "active"] (as array)Note
7οΈβ£ Manipulating Attributes
HTML attributes like id, class, src, href, disabled, and placeholder can all be read, set, and removed using the DOM attribute API.
Core Attribute Methods
attributes.js
const img = document.querySelector("img");
const link = document.querySelector("a");
const input = document.querySelector("input");
// getAttribute β read the current value
console.log(img.getAttribute("src")); // "photo.jpg"
console.log(img.getAttribute("alt")); // "A sunset"
console.log(link.getAttribute("href")); // "https://example.com"
// setAttribute β set or update a value
img.setAttribute("src", "new-photo.jpg");
img.setAttribute("alt", "A mountain");
link.setAttribute("href", "https://new-site.com");
link.setAttribute("target", "_blank");
// removeAttribute β completely remove the attribute
img.removeAttribute("title");
input.removeAttribute("disabled"); // Enables the input
// hasAttribute β check if attribute exists (returns boolean)
if (link.hasAttribute("target")) {
console.log("Opens in a new tab");
}
// getAttributeNames β list all attributes
console.log(img.getAttributeNames());
// ["src", "alt", "class", "id"]Direct Property Access
Many common attributes are also accessible directly as element properties. This is often shorter and cleaner:
direct-properties.js
const input = document.querySelector("input");
const img = document.querySelector("img");
const link = document.querySelector("a");
const checkbox = document.querySelector("input[type='checkbox']");
// Direct property access (preferred for common attributes)
console.log(input.id); // "username"
console.log(input.type); // "text"
console.log(input.value); // Current value of the input
console.log(input.placeholder); // "Enter your name..."
console.log(input.disabled); // false
console.log(input.required); // true
console.log(img.src); // Full URL (not relative path!)
console.log(img.alt); // "Description"
console.log(link.href); // Full URL
console.log(link.target); // "_blank"
console.log(checkbox.checked); // true or false
// Setting via properties
input.value = "new value";
input.disabled = true; // Disables the input
checkbox.checked = true; // Checks the checkboxData Attributes
HTML5 data attributes (prefixed with data-) let you store custom data directly on elements. They are accessible via the dataset property.
data-attributes.js
// HTML: <div class="user-card" data-user-id="42" data-role="admin" data-is-active="true">
const card = document.querySelector(".user-card");
// Read data attributes via dataset (camelCase access)
console.log(card.dataset.userId); // "42" (data-user-id)
console.log(card.dataset.role); // "admin" (data-role)
console.log(card.dataset.isActive); // "true" (data-is-active)
// Set a data attribute
card.dataset.score = "100"; // Creates data-score="100" in HTML
card.dataset.lastLogin = "2024"; // Creates data-last-login="2024"
// Remove a data attribute
delete card.dataset.role;
// Iterate over all data attributes
for (const [key, value] of Object.entries(card.dataset)) {
console.log(key, value);
}
// Practical use: store an ID on a button to know which item to delete
const deleteBtn = document.querySelector(".delete-btn");
deleteBtn.dataset.itemId = "5";
deleteBtn.addEventListener("click", (e) => {
const id = e.target.dataset.itemId;
deleteItemById(id); // Use the stored ID
});Note
8οΈβ£ Creating and Inserting Elements
One of the most powerful features of the DOM is the ability to create brand-new HTML elements with JavaScript and insert them anywhere on the page β without reloading.
createElement β Create a New Element
create-element.js
// Create any HTML element by tag name
const div = document.createElement("div");
const p = document.createElement("p");
const img = document.createElement("img");
const button = document.createElement("button");
const input = document.createElement("input");
const ul = document.createElement("ul");
// The element exists in memory but is NOT in the DOM yet
// You must configure and insert it
// Configure the element
div.id = "new-card";
div.className = "card active";
div.textContent = "Hello, I'm a new card!";
button.textContent = "Click Me";
button.type = "button";
button.classList.add("btn", "btn-primary");
input.type = "email";
input.placeholder = "Enter your email";
input.required = true;
img.src = "photo.jpg";
img.alt = "A beautiful photo";Inserting Elements into the DOM
insert-elements.js
const container = document.querySelector("#container");
const newCard = document.createElement("div");
newCard.className = "card";
newCard.textContent = "New Card";
// appendChild β insert as the LAST child
container.appendChild(newCard);
// prepend β insert as the FIRST child
container.prepend(newCard);
// append β can insert both nodes and text strings
container.append(newCard);
container.append("plain text", newCard, "more text");
// before β insert immediately before the element
const existingCard = document.querySelector(".card");
existingCard.before(newCard); // inserts newCard before existingCard
// after β insert immediately after the element
existingCard.after(newCard); // inserts newCard after existingCard
// insertBefore β classic method, insert before a reference node
container.insertBefore(newCard, existingCard);insertAdjacentHTML β Insert Raw HTML Strings
insertAdjacentHTML is the fastest way to insert HTML strings at a precise position without destroying existing elements or their event listeners.
insert-adjacent.js
const target = document.querySelector("#target");
// "beforebegin" β insert BEFORE the target element itself
target.insertAdjacentHTML("beforebegin", "<p>Before target</p>");
// "afterbegin" β insert INSIDE target, BEFORE its first child
target.insertAdjacentHTML("afterbegin", "<p>First inside target</p>");
// "beforeend" β insert INSIDE target, AFTER its last child
target.insertAdjacentHTML("beforeend", "<p>Last inside target</p>");
// "afterend" β insert AFTER the target element itself
target.insertAdjacentHTML("afterend", "<p>After target</p>");
// insertAdjacentElement β same positions but inserts a DOM node
const newEl = document.createElement("span");
target.insertAdjacentElement("beforeend", newEl);
// insertAdjacentText β same positions but inserts plain text
target.insertAdjacentText("beforeend", "Appended text");Positions Explained
positions
<!-- beforebegin -->
<div id="target"> <!-- afterbegin -->
existing content
<!-- beforeend -->
</div>
<!-- afterend -->Cloning Elements
Use cloneNode to create a copy of an existing element:
clone-node.js
const template = document.querySelector(".card-template");
// cloneNode(false) β shallow clone, copies only the element, not children
const shallowClone = template.cloneNode(false);
// cloneNode(true) β deep clone, copies element AND all its children
const deepClone = template.cloneNode(true);
// Modify the clone so it's unique
deepClone.querySelector("h3").textContent = "New Card Title";
deepClone.removeAttribute("hidden");
// Add the clone to the DOM
document.querySelector("#cards").appendChild(deepClone);
// Practical: generate a list of cards from an array
const users = ["Alice", "Bob", "Charlie"];
const container = document.querySelector("#users");
const template2 = document.querySelector(".user-template");
users.forEach(name => {
const clone = template2.cloneNode(true);
clone.querySelector(".name").textContent = name;
container.appendChild(clone);
});DocumentFragment β Batch Inserts for Performance
Every time you insert an element into the DOM, the browser repaints the page. When inserting many elements, use a DocumentFragment to batch them into a single DOM update.
document-fragment.js
// BAD β causes 100 individual repaints
const list = document.querySelector("ul");
for (let i = 0; i < 100; i++) {
const li = document.createElement("li");
li.textContent = "Item " + i;
list.appendChild(li); // DOM update on every iteration!
}
// GOOD β causes only 1 repaint
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const li = document.createElement("li");
li.textContent = "Item " + i;
fragment.appendChild(li); // Appending to fragment β no repaint
}
list.appendChild(fragment); // Single DOM update β much faster!Note
9οΈβ£ Removing and Replacing Elements
Removing Elements
remove.js
const item = document.querySelector(".item");
// Modern β remove directly (ES2019+, widely supported)
item.remove();
// Classic β remove via parent (works in all browsers)
const parent = item.parentNode;
parent.removeChild(item);
// Remove all children β three ways
const list = document.querySelector("ul");
// Method 1: innerHTML (fastest, but destroys event listeners)
list.innerHTML = "";
// Method 2: while loop (safest, preserves listeners if using delegation)
while (list.firstChild) {
list.removeChild(list.firstChild);
}
// Method 3: replaceChildren (modern, clean)
list.replaceChildren();Replacing Elements
replace.js
const oldEl = document.querySelector(".old-card");
// Create the replacement
const newEl = document.createElement("div");
newEl.className = "new-card";
newEl.textContent = "I am the replacement!";
// replaceWith β modern method, replaces element with node(s) or string(s)
oldEl.replaceWith(newEl);
oldEl.replaceWith(newEl, "some text", anotherEl); // multiple args ok
// replaceChild β classic method via parent
const parent = oldEl.parentNode;
parent.replaceChild(newEl, oldEl);
// replaceChildren β replace ALL children at once
const container = document.querySelector("#container");
container.replaceChildren(newEl); // replaces all children with newElπ±οΈ DOM Events
Events are the mechanism through which JavaScript responds to user interactions and browser actions. Almost everything interactive on the web is powered by events.
addEventListener β The Right Way
add-event-listener.js
const btn = document.querySelector("#myBtn");
// Syntax: element.addEventListener(eventType, handler, options)
// Using a regular function
btn.addEventListener("click", function(event) {
console.log("Clicked!", event);
});
// Using an arrow function
btn.addEventListener("click", (e) => {
console.log("Event type:", e.type); // "click"
console.log("Target:", e.target); // <button id="myBtn">
console.log("Mouse X:", e.clientX); // X position
});
// Using a named function (required if you want to remove it later)
function handleClick(e) {
console.log("Handled by named function");
}
btn.addEventListener("click", handleClick);
// Remove the listener β must use the SAME function reference
btn.removeEventListener("click", handleClick);
// Options object β third argument
btn.addEventListener("click", handleClick, {
once: true, // Listener fires only once, then auto-removes
passive: true, // Hints to browser you won't call preventDefault (better scroll performance)
capture: true, // Listen during capture phase instead of bubble phase
});The Event Object
Every event handler receives an event object as its first argument. It contains information about what happened:
| Property / Method | Description |
|---|---|
| e.type | Event type as string (e.g. "click", "keydown") |
| e.target | The element that originally triggered the event |
| e.currentTarget | The element the listener is attached to |
| e.timeStamp | Time the event occurred (in ms from page load) |
| e.preventDefault() | Stop the browser's default action for this event |
| e.stopPropagation() | Stop the event from bubbling up or capturing down |
| e.clientX / e.clientY | Mouse position relative to the viewport (mouse events) |
| e.pageX / e.pageY | Mouse position relative to the whole document |
| e.key | Key name pressed (keyboard events), e.g. "Enter", "a" |
| e.code | Physical key code (keyboard events), e.g. "KeyA" |
| e.ctrlKey / e.shiftKey / e.altKey | Whether Ctrl/Shift/Alt was held during the event |
| e.which / e.button | Which mouse button was pressed |
Common Event Types
Mouse Events
mouse-events.js
const box = document.querySelector(".box");
box.addEventListener("click", (e) => console.log("Single click"));
box.addEventListener("dblclick", (e) => console.log("Double click"));
box.addEventListener("mousedown", (e) => console.log("Mouse button pressed"));
box.addEventListener("mouseup", (e) => console.log("Mouse button released"));
box.addEventListener("mouseenter", (e) => console.log("Mouse entered (no bubble)"));
box.addEventListener("mouseleave", (e) => console.log("Mouse left (no bubble)"));
box.addEventListener("mouseover", (e) => console.log("Mouse over (bubbles)"));
box.addEventListener("mouseout", (e) => console.log("Mouse out (bubbles)"));
box.addEventListener("mousemove", (e) => {
console.log("Mouse at:", e.clientX, e.clientY);
});
box.addEventListener("contextmenu", (e) => {
e.preventDefault(); // Prevent right-click menu
console.log("Right clicked!");
});Keyboard Events
keyboard-events.js
document.addEventListener("keydown", (e) => {
console.log("Key pressed:", e.key); // "a", "Enter", "ArrowUp", etc.
console.log("Key code:", e.code); // "KeyA", "Enter", "ArrowUp"
// Check for specific keys
if (e.key === "Enter") {
console.log("Enter was pressed!");
}
// Check for modifier keys
if (e.ctrlKey && e.key === "s") {
e.preventDefault(); // Prevent browser save dialog
saveDocument();
}
if (e.shiftKey && e.key === "A") {
console.log("Shift + A");
}
});
document.addEventListener("keyup", (e) => {
console.log("Key released:", e.key);
});Form Events
form-events.js
const form = document.querySelector("form");
const input = document.querySelector("input");
const select = document.querySelector("select");
// submit β fires when form is submitted
form.addEventListener("submit", (e) => {
e.preventDefault(); // Stop the page from reloading
const formData = new FormData(form);
console.log(Object.fromEntries(formData));
});
// input β fires on every keystroke (real-time)
input.addEventListener("input", (e) => {
console.log("Current value:", e.target.value);
});
// change β fires when value changes AND focus leaves the element
input.addEventListener("change", (e) => {
console.log("Final value:", e.target.value);
});
select.addEventListener("change", (e) => {
console.log("Selected:", e.target.value);
});
// focus and blur β fires when element gains or loses focus
input.addEventListener("focus", () => input.classList.add("focused"));
input.addEventListener("blur", () => input.classList.remove("focused"));Window and Document Events
window-events.js
// scroll β fires as the user scrolls
window.addEventListener("scroll", () => {
const scrollY = window.scrollY;
if (scrollY > 300) {
document.querySelector(".back-to-top").classList.add("visible");
}
});
// resize β fires when the window is resized
window.addEventListener("resize", () => {
console.log("Window:", window.innerWidth, "x", window.innerHeight);
});
// DOMContentLoaded β DOM is ready (HTML parsed, no resources needed)
document.addEventListener("DOMContentLoaded", () => {
console.log("DOM ready!");
});
// load β everything loaded (images, fonts, scripts)
window.addEventListener("load", () => {
console.log("Page fully loaded");
});
// beforeunload β user is leaving the page
window.addEventListener("beforeunload", (e) => {
e.preventDefault();
e.returnValue = ""; // Shows browser confirmation dialog
});1οΈβ£1οΈβ£ Event Bubbling, Capturing, and Delegation
Event Propagation β Bubbling and Capturing
When an event fires on an element, it doesn't stay there β it propagates through the DOM in two phases:
- Capture Phase: Event travels DOWN from the window to the target element
- Target Phase: Event reaches the actual element that was clicked
- Bubble Phase: Event travels BACK UP from the target to the window
propagation.js
// HTML structure: document > body > section > div > button
const button = document.querySelector("button");
const div = document.querySelector("div");
const section = document.querySelector("section");
// By default, listeners fire during the BUBBLE phase (bottom-up)
button.addEventListener("click", () => console.log("1. Button clicked"));
div.addEventListener("click", () => console.log("2. Div received bubble"));
section.addEventListener("click", () => console.log("3. Section received bubble"));
// Click the button β logs: 1, 2, 3
// Use capture: true to listen during the CAPTURE phase (top-down)
section.addEventListener("click", () => console.log("Section (capture)"), { capture: true });
div.addEventListener("click", () => console.log("Div (capture)"), { capture: true });
button.addEventListener("click", () => console.log("Button (target)"));
// Click the button β logs: Section (capture), Div (capture), Button (target)
// stopPropagation β stops the event from bubbling further
div.addEventListener("click", (e) => {
e.stopPropagation(); // Event won't reach section
console.log("Div stopped propagation");
});Question
Some resources say event propagation has 2 phases, while others say 3 phases. ?
Both are correct, depending on what they're describing.
- 2 phases refer to the two directions the event travels:
- Capture Phase
- Bubble Phase
- 3 phases refer to the complete event flow:
- Capture Phase
- Target Phase
- Bubble Phase
The Target Phase is often not counted as a propagation phase because the event has already reached the target element.
Event Delegation
Event delegation is the technique of attaching a single event listener to a parent element and using e.target to determine which child was clicked. This is much more efficient than adding a listener to each child β and it automatically works for dynamically added children.
event-delegation.js
// BAD β attaches 100 separate listeners
const items = document.querySelectorAll(".item");
items.forEach(item => {
item.addEventListener("click", (e) => {
e.target.classList.toggle("selected");
});
});
// Also doesn't work for elements added later!
// GOOD β one listener on the parent handles everything
const list = document.querySelector("#itemList");
list.addEventListener("click", (e) => {
// Check that the click came from an <li>, not the list itself
if (e.target.tagName === "LI") {
e.target.classList.toggle("selected");
}
// More flexible: use closest() for nested elements
const item = e.target.closest(".item");
if (item) {
item.classList.toggle("selected");
}
// Handle multiple button types inside the list
if (e.target.classList.contains("delete-btn")) {
e.target.closest(".item").remove();
}
if (e.target.classList.contains("edit-btn")) {
startEditing(e.target.closest(".item"));
}
});
// Now this newly added item is automatically handled:
const newItem = document.createElement("li");
newItem.className = "item";
newItem.textContent = "New Item";
list.appendChild(newItem);Note
1οΈβ£2οΈβ£ Traversing the DOM
DOM traversal lets you navigate the tree relative to any node β moving up to parents, down to children, or sideways to siblings β without running a new querySelector.
Parent Traversal
parent-traversal.js
const item = document.querySelector(".active-item");
// parentElement β the direct parent element
console.log(item.parentElement); // <ul class="list">
// Chain to go further up
console.log(item.parentElement.parentElement); // <section>
// parentNode β similar but can return non-element nodes (e.g. document)
console.log(item.parentNode);
// closest() β walks UP the tree and returns the first ancestor
// (or self) that matches the CSS selector
const section = item.closest("section"); // Nearest <section> ancestor
const form = item.closest("form"); // Nearest <form> ancestor
const container = item.closest(".wrapper"); // Nearest .wrapper ancestor
// Practical: find the card that contains the clicked delete button
document.addEventListener("click", (e) => {
if (e.target.matches(".delete-btn")) {
const card = e.target.closest(".card");
card.remove();
}
});Child Traversal
child-traversal.js
const list = document.querySelector("ul");
// children β only element children (no text/comment nodes)
console.log(list.children); // HTMLCollection [li, li, li]
console.log(list.children[0]); // First <li>
console.log(list.children.length); // 3
// childNodes β ALL child nodes including text and comment nodes
console.log(list.childNodes); // NodeList [text, li, text, li, text]
// firstElementChild / lastElementChild β first and last element children
console.log(list.firstElementChild); // First <li>
console.log(list.lastElementChild); // Last <li>
// firstChild / lastChild β first and last child node (may be text node!)
console.log(list.firstChild); // Likely a text node (#text)
// hasChildNodes β check if element has any children
console.log(list.hasChildNodes()); // true
// childElementCount β number of child elements
console.log(list.childElementCount); // 3Sibling Traversal
sibling-traversal.js
const currentItem = document.querySelector(".active");
// nextElementSibling β the next element sibling
console.log(currentItem.nextElementSibling); // <li> after current
// previousElementSibling β the previous element sibling
console.log(currentItem.previousElementSibling); // <li> before current
// nextSibling / previousSibling β can be text nodes, not just elements
console.log(currentItem.nextSibling); // Might be a text node
// Practical: move highlight through a list
function highlightNext() {
const active = document.querySelector(".active");
const next = active.nextElementSibling;
if (next) {
active.classList.remove("active");
next.classList.add("active");
}
}
function highlightPrev() {
const active = document.querySelector(".active");
const prev = active.previousElementSibling;
if (prev) {
active.classList.remove("active");
prev.classList.add("active");
}
}
document.addEventListener("keydown", (e) => {
if (e.key === "ArrowDown") highlightNext();
if (e.key === "ArrowUp") highlightPrev();
});el.matches() β Test an Element Against a Selector
matches.js
const el = document.querySelector(".card");
// Returns true if the element matches the CSS selector
console.log(el.matches(".card")); // true
console.log(el.matches(".card.active")); // true or false
console.log(el.matches("div")); // true if el is a <div>
console.log(el.matches(":hover")); // true if currently hovered
// Practical use in event delegation
document.addEventListener("click", (e) => {
if (e.target.matches(".btn-primary")) {
// Handle primary button click
}
if (e.target.matches("nav > a")) {
// Handle nav link click
}
});1οΈβ£3οΈβ£ Element Dimensions and Position
The DOM gives you precise measurements of element sizes and positions on the page.
Size Properties
| Property | Includes Padding | Includes Border | Includes Margin | Includes Scrolled |
|---|---|---|---|---|
| clientWidth/Height | β | β | β | β |
| offsetWidth/Height | β | β | β | β |
| scrollWidth/Height | β | β | β | β |
dimensions.js
const box = document.querySelector(".box");
// clientWidth/Height β inner size (padding included, border excluded)
console.log(box.clientWidth); // e.g. 280
console.log(box.clientHeight); // e.g. 140
// offsetWidth/Height β outer size (padding + border included)
console.log(box.offsetWidth); // e.g. 300 (adds 10px border each side)
console.log(box.offsetHeight); // e.g. 160
// scrollWidth/Height β total scrollable size including hidden overflow
console.log(box.scrollWidth); // e.g. 500 (content is 500px wide)
console.log(box.scrollHeight); // e.g. 800
// Scroll position of a scrollable element
console.log(box.scrollTop); // How far scrolled vertically
console.log(box.scrollLeft); // How far scrolled horizontally
// Page-level scroll position
console.log(window.scrollX); // Same as window.pageXOffset
console.log(window.scrollY); // Same as window.pageYOffsetgetBoundingClientRect β Position Relative to Viewport
bounding-rect.js
const card = document.querySelector(".card");
// Returns a DOMRect object with position and size info
const rect = card.getBoundingClientRect();
console.log(rect.top); // Distance from top of viewport
console.log(rect.bottom); // Distance from top of viewport to element bottom
console.log(rect.left); // Distance from left of viewport
console.log(rect.right); // Distance from left of viewport to element right
console.log(rect.width); // Element width (same as offsetWidth usually)
console.log(rect.height); // Element height
console.log(rect.x); // Same as rect.left
console.log(rect.y); // Same as rect.top
// Check if element is visible in the viewport
function isInViewport(el) {
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth
);
}
// Scroll to an element smoothly
card.scrollIntoView({ behavior: "smooth", block: "center" });
// Scroll the page to a specific position
window.scrollTo({ top: 500, behavior: "smooth" });
window.scrollBy({ top: 100, behavior: "smooth" }); // Relative scroll1οΈβ£4οΈβ£ Working with Forms
Forms are one of the most important parts of any web application. The DOM makes it easy to read, validate, and submit form data.
Reading Form Values
form-values.js
// Text inputs, textarea, password, email, number
const textInput = document.querySelector("input[type='text']");
console.log(textInput.value); // Current value as string
textInput.value = "New value"; // Set the value
// Checkbox
const checkbox = document.querySelector("input[type='checkbox']");
console.log(checkbox.checked); // true or false
checkbox.checked = true; // Check it programmatically
// Radio buttons β find which one is selected
const radios = document.querySelectorAll("input[name='gender']");
const selected = [...radios].find(r => r.checked);
console.log(selected?.value); // "male" or "female" etc.
// Select dropdown
const select = document.querySelector("select");
console.log(select.value); // Value of selected option
console.log(select.selectedIndex); // Index of selected option
console.log(select.options[select.selectedIndex].text); // Text of selected option
// Select multiple
const multiSelect = document.querySelector("select[multiple]");
const selectedValues = [...multiSelect.options]
.filter(opt => opt.selected)
.map(opt => opt.value);
// File input
const fileInput = document.querySelector("input[type='file']");
console.log(fileInput.files[0]); // First selected file
console.log(fileInput.files[0].name, fileInput.files[0].size);FormData API
form-data.js
const form = document.querySelector("form");
form.addEventListener("submit", (e) => {
e.preventDefault();
// FormData automatically collects all named form fields
const formData = new FormData(form);
// Access individual values
console.log(formData.get("username"));
console.log(formData.get("email"));
// Convert to a plain object
const data = Object.fromEntries(formData);
console.log(data);
// { username: "Alice", email: "alice@example.com", ... }
// Get all values for a field (e.g. checkboxes with same name)
console.log(formData.getAll("interests"));
// ["coding", "music", "gaming"]
// Send to server
fetch("/api/submit", {
method: "POST",
body: formData, // Can send FormData directly to fetch
});
});Form Validation
form-validation.js
const form = document.querySelector("form");
const emailInput = document.querySelector("#email");
const passwordInput = document.querySelector("#password");
// Check native validity
console.log(emailInput.validity.valid); // true or false
console.log(emailInput.validity.typeMismatch); // true if not an email
console.log(emailInput.validity.valueMissing); // true if required and empty
console.log(emailInput.validationMessage); // Browser message string
// Custom validation
emailInput.addEventListener("input", () => {
if (!emailInput.value.includes("@")) {
emailInput.setCustomValidity("Please enter a valid email address.");
} else {
emailInput.setCustomValidity(""); // Clear custom error
}
});
// Manual validation on submit
form.addEventListener("submit", (e) => {
e.preventDefault();
const errors = [];
if (!emailInput.value) {
errors.push("Email is required");
}
if (passwordInput.value.length < 8) {
errors.push("Password must be at least 8 characters");
}
if (errors.length > 0) {
document.querySelector("#error-list").textContent = errors.join(", ");
return;
}
// All valid β submit the form
form.submit();
});1οΈβ£5οΈβ£ Real-World Project β Dynamic To-Do List
Let's build a fully working to-do list application using every DOM concept covered in this tutorial. No libraries, no frameworks β pure JavaScript and DOM manipulation.
HTML Structure
todo.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DOM To-Do List</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; max-width: 600px; margin: 2rem auto; padding: 1rem; }
h1 { margin-bottom: 1.5rem; }
.input-row { display: flex; gap: 8px; margin-bottom: 1.5rem; }
.input-row input { flex: 1; padding: 10px; border: 1px solid #ccc; border-radius: 6px; font-size: 16px; }
.input-row button { padding: 10px 20px; background: #3b82f6; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; }
#taskList { list-style: none; }
.task-item { display: flex; align-items: center; gap: 10px; padding: 12px; border-bottom: 1px solid #e5e7eb; }
.task-item.done .task-text { text-decoration: line-through; color: #9ca3af; }
.task-text { flex: 1; }
.task-item button { padding: 4px 10px; border: 1px solid; border-radius: 4px; cursor: pointer; font-size: 13px; }
.done-btn { border-color: #10b981; color: #10b981; background: transparent; }
.delete-btn { border-color: #ef4444; color: #ef4444; background: transparent; }
.filters { display: flex; gap: 8px; margin-bottom: 1rem; }
.filters button { padding: 6px 14px; border: 1px solid #ccc; border-radius: 20px; background: transparent; cursor: pointer; }
.filters button.active { background: #3b82f6; color: white; border-color: #3b82f6; }
#taskCount { font-size: 13px; color: #6b7280; margin-bottom: 1rem; }
#emptyMsg { text-align: center; color: #9ca3af; padding: 2rem; display: none; }
</style>
</head>
<body>
<h1>π My To-Do List</h1>
<div class="input-row">
<input type="text" id="taskInput" placeholder="Add a new task..." />
<button id="addBtn">Add Task</button>
</div>
<div class="filters">
<button class="active" data-filter="all">All</button>
<button data-filter="active">Active</button>
<button data-filter="done">Done</button>
</div>
<p id="taskCount"></p>
<ul id="taskList"></ul>
<p id="emptyMsg">No tasks yet. Add one above! π</p>
<script src="todo.js"></script>
</body>
</html>JavaScript Logic
todo.js
// ββ State ββββββββββββββββββββββββββββββββββββββ
let tasks = [];
let currentFilter = "all";
let nextId = 1;
// ββ Element References ββββββββββββββββββββββββββ
const taskInput = document.getElementById("taskInput");
const addBtn = document.getElementById("addBtn");
const taskList = document.getElementById("taskList");
const taskCount = document.getElementById("taskCount");
const emptyMsg = document.getElementById("emptyMsg");
const filterBtns = document.querySelectorAll(".filters button");
// ββ Add a Task ββββββββββββββββββββββββββββββββββ
function addTask() {
const text = taskInput.value.trim();
if (!text) {
taskInput.focus();
return;
}
tasks.push({ id: nextId++, text, done: false });
taskInput.value = "";
taskInput.focus();
render();
}
// ββ Toggle Done State βββββββββββββββββββββββββββ
function toggleTask(id) {
tasks = tasks.map(task =>
task.id === id ? { ...task, done: !task.done } : task
);
render();
}
// ββ Delete a Task βββββββββββββββββββββββββββββββ
function deleteTask(id) {
tasks = tasks.filter(task => task.id !== id);
render();
}
// ββ Filter Tasks ββββββββββββββββββββββββββββββββ
function getFilteredTasks() {
if (currentFilter === "active") return tasks.filter(t => !t.done);
if (currentFilter === "done") return tasks.filter(t => t.done);
return tasks; // "all"
}
// ββ Render ββββββββββββββββββββββββββββββββββββββ
function render() {
const filtered = getFilteredTasks();
// Update task count
const remaining = tasks.filter(t => !t.done).length;
taskCount.textContent = `${remaining} task${remaining !== 1 ? "s" : ""} remaining`;
// Show/hide empty state
emptyMsg.style.display = filtered.length === 0 ? "block" : "none";
// Build the list using DocumentFragment (performance)
const fragment = document.createDocumentFragment();
filtered.forEach(task => {
const li = document.createElement("li");
li.className = `task-item${task.done ? " done" : ""}`;
li.dataset.id = task.id;
li.innerHTML = `
<span class="task-text">${task.text}</span>
<button class="done-btn">${task.done ? "β© Undo" : "β
Done"}</button>
<button class="delete-btn">ποΈ Delete</button>
`;
fragment.appendChild(li);
});
taskList.innerHTML = ""; // Clear existing list
taskList.appendChild(fragment);
}
// ββ Event Listeners βββββββββββββββββββββββββββββ
// Add task on button click
addBtn.addEventListener("click", addTask);
// Add task on Enter key
taskInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") addTask();
});
// Event delegation β handles done and delete for all tasks
taskList.addEventListener("click", (e) => {
const item = e.target.closest(".task-item");
if (!item) return;
const id = parseInt(item.dataset.id);
if (e.target.classList.contains("done-btn")) {
toggleTask(id);
}
if (e.target.classList.contains("delete-btn")) {
deleteTask(id);
}
});
// Filter buttons
filterBtns.forEach(btn => {
btn.addEventListener("click", () => {
filterBtns.forEach(b => b.classList.remove("active"));
btn.classList.add("active");
currentFilter = btn.dataset.filter;
render();
});
});
// ββ Initialize βββββββββββββββββββββββββββββββββββ
render();Note
1οΈβ£6οΈβ£ Best Practices and Performance Tips
π Performance Tips
- Minimize DOM access. Every query is a search through the tree. Cache your element references in variables instead of calling querySelector repeatedly in loops.
cache-elements.js
// BAD β queries the DOM on every iteration for (let i = 0; i < 1000; i++) { document.querySelector("#counter").textContent = i; } // GOOD β query once, reuse the reference const counter = document.querySelector("#counter"); for (let i = 0; i < 1000; i++) { counter.textContent = i; } - Batch DOM writes with DocumentFragment. Inserting many elements one at a time causes many repaints. Use a DocumentFragment to batch all changes into a single DOM update.
- Use event delegation. One parent listener is far better than hundreds of individual listeners on child elements. It also handles dynamically added children automatically.
- Avoid layout thrashing. Don't mix reading layout properties (like getBoundingClientRect, offsetWidth) with writing styles in the same loop β this forces the browser to recalculate layout on every iteration.
layout-thrashing.js
const boxes = document.querySelectorAll(".box"); // BAD β read then write, then read, then write (layout thrashing) boxes.forEach(box => { const height = box.offsetHeight; // Read β forces layout calculation box.style.height = height * 2 + "px"; // Write β invalidates layout }); // GOOD β read all first, then write all const heights = [...boxes].map(box => box.offsetHeight); // All reads boxes.forEach((box, i) => { box.style.height = heights[i] * 2 + "px"; // All writes }); - Use requestAnimationFrame for visual updates. When animating or doing visual updates driven by events like scroll or resize, wrap your DOM update in requestAnimationFrame to sync with the browser's paint cycle.
raf.js
window.addEventListener("scroll", () => { requestAnimationFrame(() => { const scrolled = window.scrollY; document.querySelector(".progress-bar").style.width = scrolled + "px"; }); });
β Code Quality Best Practices
- Always check if an element exists before operating on it: if (el) { el.textContent = "..."; } β avoid null reference errors.
- Use semantic HTML first. A properly structured HTML document is easier to traverse and select. Use IDs for unique elements, classes for groups.
- Never use inline event handlers in HTML like onclick="doSomething()". Always use addEventListener in your JavaScript file for cleaner separation of concerns.
- Prefer textContent over innerHTML whenever you don't need to render HTML β it's faster and safe from XSS attacks.
- Clean up event listeners when elements are removed from the DOM to prevent memory leaks. Use removeEventListener with the same named function reference.
- Use const for element references. DOM element references themselves don't change, even though their content might. Always use const el = document.querySelector(...).
π οΈ Useful DOM Utilities to Know
| API | What It Does |
|---|---|
| MutationObserver | Watch for DOM changes (additions, removals, attribute changes) reactively |
| IntersectionObserver | Detect when elements enter or leave the viewport β ideal for lazy loading |
| ResizeObserver | Watch for changes in element size without polling |
| requestAnimationFrame | Schedule visual updates in sync with the browser's repaint cycle |
| document.createDocumentFragment() | Batch DOM insertions for a single repaint |
| el.scrollIntoView() | Smoothly scroll to any element |
| window.getComputedStyle(el) | Read final CSS values applied to an element from all sources |