🌲 DOM in JavaScript β€” Complete Tutorial

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.

>>The DOM is the bridge between your HTML and your JavaScript. Master it, and you can build anything in the browser.

πŸ“– Table of Contents

  1. What is the DOM?
  2. The DOM Tree Structure
  3. Accessing the DOM β€” The document Object
  4. Selecting Elements
  5. Manipulating Content
  6. Manipulating Styles and Classes
  7. Manipulating Attributes
  8. Creating and Inserting Elements
  9. Removing and Replacing Elements
  10. DOM Events
  11. Event Bubbling, Capturing, and Delegation
  12. Traversing the DOM
  13. Element Dimensions and Position
  14. Working with Forms
  15. Real-World Project β€” Dynamic To-Do List
  16. 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

πŸ“Œ The DOM is language-independent β€” it can technically be used with any language, but JavaScript is by far the most common.

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 TypeConstantExample
Element NodeNode.ELEMENT_NODE (1)<div>, <p>, <h1>
Text NodeNode.TEXT_NODE (3)The text inside an element
Comment NodeNode.COMMENT_NODE (8)<!-- a comment -->
Document NodeNode.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

πŸ’‘ Text content inside elements is stored as a separate Text Node, which is a child of the element node. This is why el.childNodes may return more nodes than el.children.

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> elements

DOMContentLoaded 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

πŸ”₯ Always prefer DOMContentLoaded for DOM manipulation β€” it fires faster than load since it doesn't wait for images or stylesheets to download.

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

MethodSelects ByReturnsLive?
getElementById(id)id attributeElement or nullN/A
getElementsByClassName(cls)class nameHTMLCollectionβœ… Live
getElementsByTagName(tag)tag nameHTMLCollectionβœ… Live
getElementsByName(name)name attributeNodeListβœ… Live
querySelector(selector)First CSS matchElement or null❌ Static
querySelectorAll(selector)All CSS matchesNodeList❌ 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);         // null

getElementsByClassName

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

πŸ’‘ Live vs Static: HTMLCollection (from getElementsBy*) updates automatically when the DOM changes. NodeList (from querySelectorAll) is a snapshot taken at the time of the call β€” it won't reflect later DOM changes.

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

⚠️ Security Warning: Never set innerHTML using untrusted user input. A malicious user could inject a <script> tag or event handler attribute and execute arbitrary code β€” this is called XSS (Cross-Site Scripting).

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 safe

innerText

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 source

Comparison Table

PropertyParses HTMLXSS SafeCSS AwarePerformance
innerHTMLβœ… Yes⚠️ No❌ NoMedium
textContent❌ Noβœ… Yes❌ NoFast
innerText❌ Noβœ… Yesβœ… YesSlow (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

πŸ’‘ Best Practice: Define all your visual states in CSS classes and use classList to switch between them. Avoid setting el.style directly unless the value must be calculated dynamically (e.g. from user input or math).

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 checkbox

Data 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

πŸ“Œ All dataset values are always strings. If you store a number, remember to parse it: parseInt(card.dataset.userId).

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

πŸ”₯ DocumentFragment is a lightweight container that lives in memory, not in the DOM. Appending to it causes zero repaints. When you insert the fragment, all its children move into the DOM in a single operation.

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 / MethodDescription
e.typeEvent type as string (e.g. "click", "keydown")
e.targetThe element that originally triggered the event
e.currentTargetThe element the listener is attached to
e.timeStampTime 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.clientYMouse position relative to the viewport (mouse events)
e.pageX / e.pageYMouse position relative to the whole document
e.keyKey name pressed (keyboard events), e.g. "Enter", "a"
e.codePhysical key code (keyboard events), e.g. "KeyA"
e.ctrlKey / e.shiftKey / e.altKeyWhether Ctrl/Shift/Alt was held during the event
e.which / e.buttonWhich 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:

  1. Capture Phase: Event travels DOWN from the window to the target element
  2. Target Phase: Event reaches the actual element that was clicked
  3. 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

πŸ”₯ Event delegation is one of the most important DOM performance patterns. Always use it for lists, tables, and any repeated elements β€” especially dynamic ones.

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);   // 3

Sibling 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

PropertyIncludes PaddingIncludes BorderIncludes MarginIncludes 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.pageYOffset

getBoundingClientRect β€” 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 scroll

1️⃣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

🧠 This project uses: getElementById, querySelectorAll, createElement, DocumentFragment, innerHTML, dataset, classList, addEventListener, event delegation, closest(), remove(), and template literals β€” the full DOM toolkit in action.

1️⃣6️⃣ Best Practices and Performance Tips

πŸš€ Performance Tips

  1. 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;
    }
  2. 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.
  3. Use event delegation. One parent listener is far better than hundreds of individual listeners on child elements. It also handles dynamically added children automatically.
  4. 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
    });
  5. 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

  1. Always check if an element exists before operating on it: if (el) { el.textContent = "..."; } β€” avoid null reference errors.
  2. Use semantic HTML first. A properly structured HTML document is easier to traverse and select. Use IDs for unique elements, classes for groups.
  3. Never use inline event handlers in HTML like onclick="doSomething()". Always use addEventListener in your JavaScript file for cleaner separation of concerns.
  4. Prefer textContent over innerHTML whenever you don't need to render HTML β€” it's faster and safe from XSS attacks.
  5. Clean up event listeners when elements are removed from the DOM to prevent memory leaks. Use removeEventListener with the same named function reference.
  6. 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

APIWhat It Does
MutationObserverWatch for DOM changes (additions, removals, attribute changes) reactively
IntersectionObserverDetect when elements enter or leave the viewport β€” ideal for lazy loading
ResizeObserverWatch for changes in element size without polling
requestAnimationFrameSchedule 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
>>You don't need React or Vue to build powerful UIs. A deep understanding of the DOM gives you a superpower that framework users often don't have. πŸ’ͺ

Note

πŸ“Œ What to Learn Next: Now that you've mastered the DOM, explore Fetch API for loading data from servers, Web Storage API (localStorage / sessionStorage) for persisting data in the browser, and Web Components for building reusable custom elements using only browser APIs.