What is the DOM?
The Document Object Model (DOM) is a programming interface for web documents. It represents the page as a tree of objects that JavaScript can manipulate.
<!DOCTYPE html>
<html>
<head>
<title>My Page</title>
</head>
<body>
<h1>Hello World</h1>
<p id="intro">Welcome to my site</p>
<ul class="list">
<li>Item 1</li>
<li>Item 2</li>
</ul>
</body>
</html>
// The DOM represents this as a tree:
// document
// └── html
// ├── head
// │ └── title
// └── body
// ├── h1
// ├── p#intro
// └── ul.list
// ├── li
// └── li
// Access document object
console.log(document);
console.log(document.title); // "My Page"
console.log(document.body); // element
console.log(document.head); // element
console.log(document.documentElement); // element
Selecting Elements
JavaScript provides several methods to find and select DOM elements.
// getElementById - select by ID (fastest)
const intro = document.getElementById("intro");
console.log(intro); //
// querySelector - select first match (CSS selectors)
const heading = document.querySelector("h1");
const firstItem = document.querySelector(".list li");
const byId = document.querySelector("#intro");
const complex = document.querySelector("ul.list > li:first-child");
// querySelectorAll - select ALL matches (returns NodeList)
const allItems = document.querySelectorAll(".list li");
console.log(allItems.length); // 2
// Convert NodeList to Array for array methods
const itemsArray = [...allItems];
// or: Array.from(allItems)
// getElementsByClassName - returns HTMLCollection (live)
const lists = document.getElementsByClassName("list");
// getElementsByTagName - returns HTMLCollection (live)
const paragraphs = document.getElementsByTagName("p");
// Searching within an element
const list = document.querySelector(".list");
const nestedItem = list.querySelector("li"); // Search inside list only
// Checking if element exists
const maybeElement = document.querySelector(".nonexistent");
if (maybeElement) {
console.log("Found it!");
} else {
console.log("Not found"); // This runs
}
Use querySelector and querySelectorAll for most cases—they're flexible and use familiar CSS selectors. Use getElementById for simple ID lookups (slightly faster).
Modifying Content
Change text, HTML, and attributes of elements.
const element = document.querySelector("#intro");
// textContent - get/set text (ignores HTML)
console.log(element.textContent); // "Welcome to my site"
element.textContent = "New text content";
element.textContent = "Bold"; // Shows as plain text, not HTML
// innerHTML - get/set HTML (parses HTML)
element.innerHTML = "Bold text"; // Renders bold
element.innerHTML = ""; // Clear all content
// innerText - similar to textContent but respects CSS
// (hidden elements won't show)
// outerHTML - includes the element itself
console.log(element.outerHTML); // "...
"
// Working with attributes
const link = document.querySelector("a");
// getAttribute / setAttribute
console.log(link.getAttribute("href"));
link.setAttribute("href", "https://new-url.com");
link.setAttribute("target", "_blank");
// Direct property access (for standard attributes)
link.href = "https://example.com";
link.id = "myLink";
// hasAttribute / removeAttribute
if (link.hasAttribute("target")) {
link.removeAttribute("target");
}
// data-* attributes
//
const div = document.querySelector("div");
console.log(div.dataset.userId); // "123"
console.log(div.dataset.role); // "admin"
div.dataset.newAttr = "value"; // Adds data-new-attr="value"
Modifying Styles and Classes
Change the appearance of elements with styles and CSS classes.
JavaScript
const box = document.querySelector(".box");
// Inline styles (camelCase for CSS properties)
box.style.backgroundColor = "blue";
box.style.fontSize = "20px";
box.style.marginTop = "10px";
box.style.display = "flex";
// Multiple styles at once
box.style.cssText = "background: red; padding: 10px; border: 1px solid black;";
// Get computed styles (actual rendered values)
const styles = getComputedStyle(box);
console.log(styles.width);
console.log(styles.backgroundColor);
// classList - manage CSS classes (preferred!)
box.classList.add("active"); // Add class
box.classList.remove("hidden"); // Remove class
box.classList.toggle("visible"); // Toggle on/off
box.classList.replace("old", "new"); // Replace class
// Check if class exists
if (box.classList.contains("active")) {
console.log("Box is active!");
}
// Add/remove multiple classes
box.classList.add("class1", "class2", "class3");
box.classList.remove("class1", "class2");
// className - full class string (use classList instead)
console.log(box.className); // "box active visible"
box.className = "completely-new-classes"; // Replaces all
// Toggle class conditionally
const isLoggedIn = true;
box.classList.toggle("logged-in", isLoggedIn); // Add if true
box.classList.toggle("logged-out", !isLoggedIn); // Remove if false
Classes vs Inline Styles
Prefer using CSS classes over inline styles. Classes are easier to maintain, more performant, and keep your styles separate from JavaScript logic.
Creating and Removing Elements
Dynamically add and remove elements from the page.
JavaScript
// Create new element
const newParagraph = document.createElement("p");
newParagraph.textContent = "I'm a new paragraph!";
newParagraph.id = "new-para";
newParagraph.classList.add("highlight");
// Append to parent
const container = document.querySelector(".container");
container.appendChild(newParagraph);
// Insert at specific positions
const list = document.querySelector("ul");
const newItem = document.createElement("li");
newItem.textContent = "New Item";
list.prepend(newItem); // First child
list.append(newItem); // Last child
list.before(newItem); // Before the list
list.after(newItem); // After the list
// Insert relative to specific element
const secondItem = list.querySelector("li:nth-child(2)");
secondItem.before(newItem); // Before second item
secondItem.after(newItem); // After second item
// insertBefore (older method)
list.insertBefore(newItem, secondItem);
// insertAdjacentHTML - insert HTML string
container.insertAdjacentHTML("beforeend", "HTML string
");
// Positions: "beforebegin", "afterbegin", "beforeend", "afterend"
// Create multiple elements efficiently
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const li = document.createElement("li");
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
list.appendChild(fragment); // Single DOM update!
// Cloning elements
const clone = newParagraph.cloneNode(true); // true = deep clone
container.appendChild(clone);
// Removing elements
newParagraph.remove(); // Modern way
// Older way (still works)
container.removeChild(newParagraph);
// Replace element
const oldElement = document.querySelector(".old");
const newElement = document.createElement("div");
oldElement.replaceWith(newElement);
// Clear all children
container.innerHTML = "";
// or
while (container.firstChild) {
container.removeChild(container.firstChild);
}
Traversing the DOM
Navigate between elements using parent, child, and sibling relationships.
JavaScript
// Sample structure:
//
// Title
// Paragraph 1
// Paragraph 2
//
const para = document.querySelector("p");
// Parent navigation
console.log(para.parentElement); //
console.log(para.parentNode); // Same (usually)
// Closest ancestor matching selector
console.log(para.closest(".container")); //
console.log(para.closest("body")); //
// Children
const container = document.querySelector(".container");
console.log(container.children); // HTMLCollection of children
console.log(container.childElementCount); // 3
console.log(container.firstElementChild); //
console.log(container.lastElementChild); //
(last one)
// All nodes (including text nodes)
console.log(container.childNodes); // Includes text nodes, comments
console.log(container.firstChild); // Could be text node (whitespace)
// Siblings
console.log(para.nextElementSibling); //
Paragraph 2
console.log(para.previousElementSibling); //
// All nodes version
console.log(para.nextSibling); // Could be text node
console.log(para.previousSibling); // Could be text node
// Practical: find all siblings
function getSiblings(element) {
return [...element.parentElement.children].filter(
child => child !== element
);
}
// Check relationships
const heading = container.querySelector("h1");
console.log(container.contains(heading)); // true
console.log(heading.contains(container)); // false
// Match against selector
console.log(para.matches("p")); // true
console.log(para.matches(".container p")); // true
Introduction to Events
Events allow you to respond to user interactions and other occurrences.
JavaScript
const button = document.querySelector("button");
// addEventListener (recommended)
button.addEventListener("click", function(event) {
console.log("Button clicked!");
console.log(event.target); // The clicked element
});
// Arrow function version
button.addEventListener("click", (e) => {
console.log("Clicked with arrow function");
});
// Named function (can be removed later)
function handleClick(event) {
console.log("Handled!");
}
button.addEventListener("click", handleClick);
// Remove event listener
button.removeEventListener("click", handleClick);
// Event object properties
document.addEventListener("click", (e) => {
console.log(e.type); // "click"
console.log(e.target); // Element that was clicked
console.log(e.currentTarget); // Element with the listener
console.log(e.clientX, e.clientY); // Mouse coordinates
console.log(e.timeStamp); // When event occurred
});
// Common events
// Mouse: click, dblclick, mouseenter, mouseleave, mousemove
// Keyboard: keydown, keyup, keypress
// Form: submit, change, input, focus, blur
// Window: load, resize, scroll, unload
// Touch: touchstart, touchmove, touchend
// Prevent default behavior
const link = document.querySelector("a");
link.addEventListener("click", (e) => {
e.preventDefault(); // Don't navigate
console.log("Link clicked but not followed");
});
const form = document.querySelector("form");
form.addEventListener("submit", (e) => {
e.preventDefault(); // Don't submit
console.log("Form handled with JS");
});
// Stop propagation (prevent bubbling)
button.addEventListener("click", (e) => {
e.stopPropagation(); // Parent won't receive event
});
Event Delegation
Attach a single event listener to a parent element to handle events from its children. This is more efficient and works with dynamically added elements.
JavaScript
// Instead of adding listeners to each item...
const items = document.querySelectorAll("li");
items.forEach(item => {
item.addEventListener("click", () => {
// Handle click
});
});
// ...add ONE listener to the parent (delegation)
const list = document.querySelector("ul");
list.addEventListener("click", (e) => {
// Check if clicked element is a list item
if (e.target.tagName === "LI") {
console.log("Clicked:", e.target.textContent);
}
});
// Using closest() for nested elements
//
// Item Title
//
//
list.addEventListener("click", (e) => {
// Find the li even if we clicked something inside it
const item = e.target.closest("li");
if (!item) return; // Clicked outside any li
// Handle delete button
if (e.target.closest(".delete")) {
item.remove();
return;
}
// Handle item click
console.log("Selected:", item);
});
// Works with dynamically added elements!
function addItem(text) {
const li = document.createElement("li");
li.innerHTML = `
${text}
`;
list.appendChild(li);
// No need to add new event listeners!
}
addItem("New Item"); // Click handling already works
Benefits of Delegation
- Fewer event listeners = better performance
- Works with dynamically added elements
- Simpler code management
- Less memory usage
Shadow DOM Introduction
The Shadow DOM provides encapsulation for web components, allowing you to create isolated DOM trees with scoped styles that don't leak out to the main document.
Creating a Shadow Root
JavaScript - Shadow DOM Basics
// Create a custom element with Shadow DOM
class MyCard extends HTMLElement {
constructor() {
super();
// Attach shadow root (mode can be 'open' or 'closed')
const shadow = this.attachShadow({ mode: 'open' });
// Create internal structure
const wrapper = document.createElement('div');
wrapper.classList.add('card');
const title = document.createElement('h3');
title.textContent = this.getAttribute('title') || 'Card Title';
const content = document.createElement('slot'); // Slot for projected content
// Add scoped styles (won't affect outside DOM)
const style = document.createElement('style');
style.textContent = `
.card {
border: 2px solid #3b82f6;
border-radius: 8px;
padding: 16px;
background: linear-gradient(135deg, #1e3a5f, #0a0a0a);
}
h3 {
color: #60a5fa;
margin: 0 0 12px 0;
}
::slotted(*) {
color: #94a3b8;
}
`;
// Assemble the shadow DOM
shadow.appendChild(style);
wrapper.appendChild(title);
wrapper.appendChild(content);
shadow.appendChild(wrapper);
}
}
// Register the custom element
customElements.define('my-card', MyCard);
// Usage in HTML:
// <my-card title="Hello World">
// <p>This content is slotted in!</p>
// </my-card>
Shadow DOM Modes
JavaScript - Open vs Closed Mode
// Open mode - shadowRoot is accessible from outside
const openElement = document.createElement('div');
const openShadow = openElement.attachShadow({ mode: 'open' });
console.log(openElement.shadowRoot); // Returns the shadow root
// Closed mode - shadowRoot is not accessible
const closedElement = document.createElement('div');
const closedShadow = closedElement.attachShadow({ mode: 'closed' });
console.log(closedElement.shadowRoot); // null
// Store reference if you need it internally
class SecureWidget extends HTMLElement {
#shadow; // Private field
constructor() {
super();
this.#shadow = this.attachShadow({ mode: 'closed' });
this.#shadow.innerHTML = `Secure content
`;
}
updateContent(text) {
this.#shadow.querySelector('p').textContent = text;
}
}
When to Use Shadow DOM
- Component libraries: Isolate styles from host page
- Widgets: Embed in any site without CSS conflicts
- Design systems: Ensure consistent rendering
- Third-party embeds: Protect from external styles
DocumentFragment for Performance
DocumentFragment is a lightweight container that holds DOM nodes. It's not part of the active DOM tree, so changes to it don't cause reflows or repaints until you append it to the document.
Basic Usage
JavaScript - DocumentFragment Basics
// ❌ Bad: Causes multiple reflows
function addItemsBad(items) {
const list = document.getElementById('list');
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
list.appendChild(li); // Reflow on each append!
});
}
// ✅ Good: Single reflow with DocumentFragment
function addItemsGood(items) {
const list = document.getElementById('list');
const fragment = document.createDocumentFragment();
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
fragment.appendChild(li); // No reflow, fragment is not in DOM
});
list.appendChild(fragment); // Single reflow when fragment is appended
}
// Test with large dataset
const data = Array.from({ length: 1000 }, (_, i) => `Item ${i + 1}`);
console.time('Without fragment');
addItemsBad(data);
console.timeEnd('Without fragment');
console.time('With fragment');
addItemsGood(data);
console.timeEnd('With fragment');
Building Complex Structures
JavaScript - Complex Fragment Construction
// Building a table with DocumentFragment
function createTable(data) {
const fragment = document.createDocumentFragment();
const table = document.createElement('table');
table.className = 'data-table';
// Create header
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
Object.keys(data[0]).forEach(key => {
const th = document.createElement('th');
th.textContent = key.charAt(0).toUpperCase() + key.slice(1);
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// Create body using another fragment for rows
const tbody = document.createElement('tbody');
const rowFragment = document.createDocumentFragment();
data.forEach(row => {
const tr = document.createElement('tr');
Object.values(row).forEach(value => {
const td = document.createElement('td');
td.textContent = value;
tr.appendChild(td);
});
rowFragment.appendChild(tr);
});
tbody.appendChild(rowFragment);
table.appendChild(tbody);
fragment.appendChild(table);
return fragment;
}
// Usage
const users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com' }
];
document.getElementById('container').appendChild(createTable(users));
Template + Fragment Pattern
JavaScript - Using Templates with Fragments
// HTML Template (content is inert, not rendered)
// <template id="card-template">
// <div class="card">
// <h3 class="card-title"></h3>
// <p class="card-body"></p>
// <button class="card-action">Learn More</button>
// </div>
// </template>
function createCards(items) {
const template = document.getElementById('card-template');
const fragment = document.createDocumentFragment();
items.forEach(item => {
// Clone the template content (deep clone)
const clone = template.content.cloneNode(true);
// Populate the clone
clone.querySelector('.card-title').textContent = item.title;
clone.querySelector('.card-body').textContent = item.description;
clone.querySelector('.card-action').dataset.id = item.id;
fragment.appendChild(clone);
});
return fragment;
}
// Template content is a DocumentFragment!
const template = document.getElementById('card-template');
console.log(template.content instanceof DocumentFragment); // true
MutationObserver
MutationObserver provides a way to watch for changes to the DOM tree. It replaces the deprecated Mutation Events and is more performant because it batches mutations into a single callback.
Basic Usage
JavaScript - MutationObserver Basics
// Create an observer instance
const observer = new MutationObserver((mutations, obs) => {
mutations.forEach(mutation => {
console.log('Mutation type:', mutation.type);
console.log('Target:', mutation.target);
if (mutation.type === 'childList') {
console.log('Added nodes:', mutation.addedNodes);
console.log('Removed nodes:', mutation.removedNodes);
} else if (mutation.type === 'attributes') {
console.log('Changed attribute:', mutation.attributeName);
console.log('Old value:', mutation.oldValue);
}
});
});
// Configuration options
const config = {
childList: true, // Watch for added/removed children
attributes: true, // Watch for attribute changes
characterData: true, // Watch for text content changes
subtree: true, // Watch all descendants, not just direct children
attributeOldValue: true, // Record old attribute values
characterDataOldValue: true, // Record old text content
attributeFilter: ['class', 'data-status'] // Only watch specific attributes
};
// Start observing
const targetNode = document.getElementById('app');
observer.observe(targetNode, config);
// Later: stop observing
// observer.disconnect();
Practical Examples
JavaScript - Real-World MutationObserver Uses
// Example 1: Auto-initialize components when added to DOM
class ComponentInitializer {
constructor() {
this.observer = new MutationObserver(this.handleMutations.bind(this));
this.observer.observe(document.body, {
childList: true,
subtree: true
});
}
handleMutations(mutations) {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
this.initializeComponent(node);
// Also check children
node.querySelectorAll?.('[data-component]').forEach(
child => this.initializeComponent(child)
);
}
});
});
}
initializeComponent(element) {
const componentType = element.dataset?.component;
if (componentType) {
console.log(`Initializing ${componentType} component`);
// Initialize based on component type
}
}
}
// Example 2: Detect when element becomes visible (lazy loading)
function observeVisibility(element, callback) {
const observer = new MutationObserver(() => {
const style = window.getComputedStyle(element);
const isVisible = style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0';
if (isVisible) {
callback(element);
observer.disconnect();
}
});
observer.observe(element, {
attributes: true,
attributeFilter: ['style', 'class']
});
return observer;
}
// Example 3: Track form changes
function trackFormChanges(form) {
const changes = [];
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type === 'attributes' &&
mutation.target.tagName === 'INPUT') {
changes.push({
field: mutation.target.name,
attribute: mutation.attributeName,
oldValue: mutation.oldValue,
newValue: mutation.target.getAttribute(mutation.attributeName),
timestamp: Date.now()
});
}
});
});
observer.observe(form, {
attributes: true,
attributeOldValue: true,
subtree: true
});
return {
getChanges: () => [...changes],
stop: () => observer.disconnect()
};
}
Waiting for Elements
JavaScript - Wait for Element to Appear
// Utility: Wait for an element to appear in the DOM
function waitForElement(selector, timeout = 5000) {
return new Promise((resolve, reject) => {
// Check if already exists
const existing = document.querySelector(selector);
if (existing) {
return resolve(existing);
}
const observer = new MutationObserver((mutations, obs) => {
const element = document.querySelector(selector);
if (element) {
obs.disconnect();
resolve(element);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Timeout handling
setTimeout(() => {
observer.disconnect();
reject(new Error(`Element ${selector} not found within ${timeout}ms`));
}, timeout);
});
}
// Usage
async function init() {
try {
const modal = await waitForElement('#dynamic-modal');
console.log('Modal appeared:', modal);
modal.classList.add('animate-in');
} catch (error) {
console.error(error.message);
}
}
// Wait for multiple elements
async function waitForAll(selectors) {
return Promise.all(selectors.map(sel => waitForElement(sel)));
}
const [header, sidebar, content] = await waitForAll([
'#header', '#sidebar', '#content'
]);
MutationObserver Best Practices
- Always disconnect: Call
disconnect() when done to prevent memory leaks
- Be specific: Use
attributeFilter to limit which attributes to watch
- Avoid subtree when possible: Watching entire subtrees is expensive
- Batch your own DOM changes: Use DocumentFragment to minimize mutation callbacks
Summary
Selecting
querySelector, querySelectorAll, getElementById
Content
textContent, innerHTML, getAttribute
Styles
classList (add/remove/toggle), style
Creating
createElement, append, remove
Traversing
parentElement, children, closest
Events
addEventListener, delegation, preventDefault