Skip to content

JavaScript & the DOM

JavaScript brings web pages to life by enabling dynamic behavior, user interaction, and real-time updates. At its core, JavaScript interacts with the web page through the DOM (Document Object Model) — a tree-structured representation of the HTML document that JavaScript can read and modify.


The DOM Tree Structure

When a browser parses an HTML document, it constructs a tree of nodes called the DOM. Each HTML element, text content, comment, and attribute becomes a node in this tree.

document
└── html
├── head
│ ├── title
│ │ └── "My Page" (text node)
│ └── meta
└── body
├── header
│ └── nav
│ └── ul
│ ├── li
│ │ └── a
│ │ └── "Home" (text node)
│ └── li
│ └── a
│ └── "About" (text node)
├── main
│ ├── h1
│ │ └── "Welcome" (text node)
│ └── p
│ └── "Hello world" (text node)
└── footer
└── p
└── "© 2025" (text node)

Node Types

Node TypenodeType ValueExample
Element1<div>, <p>, <a>
Text3"Hello world"
Comment8<!-- comment -->
Document9document
DocumentFragment11document.createDocumentFragment()

Selecting Elements

JavaScript provides multiple methods to locate DOM elements:

// By ID (returns single element or null)
const header = document.getElementById('main-header');
// By CSS selector (returns first match or null)
const firstCard = document.querySelector('.card');
const navLink = document.querySelector('nav a[href="/about"]');
// By CSS selector (returns NodeList of ALL matches)
const allCards = document.querySelectorAll('.card');
const allLinks = document.querySelectorAll('a[target="_blank"]');
// By class name (returns live HTMLCollection)
const buttons = document.getElementsByClassName('btn');
// By tag name (returns live HTMLCollection)
const paragraphs = document.getElementsByTagName('p');
// Traversing the tree
const parent = element.parentElement;
const children = element.children; // HTMLCollection (elements only)
const childNodes = element.childNodes; // NodeList (includes text nodes)
const firstChild = element.firstElementChild;
const lastChild = element.lastElementChild;
const nextSib = element.nextElementSibling;
const prevSib = element.previousElementSibling;
const closest = element.closest('.container'); // walk up the tree

Iterating Over NodeLists

// querySelectorAll returns a NodeList (iterable, but not an Array)
const cards = document.querySelectorAll('.card');
// forEach works on NodeList
cards.forEach(card => console.log(card.textContent));
// Convert to Array for full Array methods
const cardArray = Array.from(cards);
// or: const cardArray = [...cards];
cardArray.filter(card => card.classList.contains('featured'))
.map(card => card.dataset.title);

DOM Manipulation

Creating and Inserting Elements

// Create elements
const article = document.createElement('article');
const heading = document.createElement('h2');
const paragraph = document.createElement('p');
// Set content and attributes
heading.textContent = 'New Article'; // text only (safe from XSS)
paragraph.innerHTML = '<strong>Bold</strong> text'; // parses HTML (use carefully)
article.setAttribute('id', 'article-1');
article.classList.add('card', 'featured');
article.dataset.category = 'tech'; // sets data-category="tech"
// Build the tree
article.appendChild(heading);
article.appendChild(paragraph);
// Insert into the document
const container = document.querySelector('.container');
container.appendChild(article); // append as last child
container.prepend(article); // insert as first child
container.insertBefore(article, container.children[2]); // insert before 3rd child
// Modern insertion methods
container.append(article, anotherElement); // append multiple (accepts strings too)
container.before(article); // insert before container
container.after(article); // insert after container
element.replaceWith(article); // replace element
// Insert adjacent HTML/elements
container.insertAdjacentHTML('beforebegin', '<div>Before</div>');
container.insertAdjacentHTML('afterbegin', '<div>First child</div>');
container.insertAdjacentHTML('beforeend', '<div>Last child</div>');
container.insertAdjacentHTML('afterend', '<div>After</div>');

Modifying Elements

const el = document.querySelector('.card');
// Classes
el.classList.add('active', 'highlighted');
el.classList.remove('hidden');
el.classList.toggle('expanded'); // add if absent, remove if present
el.classList.replace('old', 'new');
el.classList.contains('active'); // returns boolean
// Styles (inline styles — use sparingly)
el.style.backgroundColor = '#f0f0f0';
el.style.setProperty('--custom-color', '#2563eb');
el.style.cssText = 'color: red; font-size: 18px;'; // replaces all inline styles
// Attributes
el.setAttribute('aria-expanded', 'true');
el.getAttribute('data-id');
el.removeAttribute('disabled');
el.hasAttribute('required');
// Data attributes
el.dataset.userId = '42'; // sets data-user-id="42"
console.log(el.dataset.userId); // reads data-user-id
// Content
el.textContent = 'Safe text'; // does not parse HTML
el.innerHTML = '<em>Parsed HTML</em>'; // parses and renders HTML
el.outerHTML; // element + its HTML
// Removing
el.remove(); // remove from DOM
parent.removeChild(el); // older API
el.replaceChildren(); // remove all children

Performance: Document Fragments and Batch Updates

// BAD: causes reflow on every iteration
const list = document.querySelector('ul');
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
list.appendChild(li); // triggers layout recalculation each time
}
// GOOD: batch with DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li); // no reflow — fragment is in memory
}
list.appendChild(fragment); // single reflow when inserted
// ALSO GOOD: build HTML string
const items = Array.from(
{ length: 1000 },
(_, i) => `<li>Item ${i}</li>`
).join('');
list.innerHTML = items; // single parse and render

Event Handling

Events are the primary mechanism for user interaction in the browser.

addEventListener

const button = document.querySelector('#submit-btn');
// Add event listener
button.addEventListener('click', function(event) {
console.log('Button clicked!');
console.log('Target:', event.target); // element that fired the event
console.log('CurrentTarget:', event.currentTarget); // element listener is on
console.log('Type:', event.type); // 'click'
console.log('Timestamp:', event.timeStamp);
});
// With options
button.addEventListener('click', handler, {
once: true, // automatically removes after first call
passive: true, // promises not to call preventDefault (for scroll perf)
capture: false, // use bubbling phase (default)
signal: controller.signal // AbortController for cleanup
});
// Remove event listener (must pass same function reference)
function handleClick(event) {
console.log('clicked');
}
button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick);
// Using AbortController for cleanup
const controller = new AbortController();
button.addEventListener('click', handleClick, { signal: controller.signal });
// Later: remove all listeners attached with this controller
controller.abort();

Event Propagation: Bubbling and Capturing

When an event fires, it travels through the DOM in three phases:

┌─────────────────────────────┐
│ document │
│ ┌───────────────────────┐ │
│ │ body │ │
CAPTURING PHASE │ │ ┌─────────────────┐ │ │ BUBBLING PHASE
(top → target) │ │ │ .container │ │ │ (target → top)
│ │ │ │ ┌───────────┐ │ │ │ ▲
│ │ │ │ │ button ●──┼──┼──┼───┼─────────┘
│ │ │ │ │ (target) │ │ │ │
▼ │ │ │ └───────────┘ │ │ │
───────────────▶ │ │ └─────────────────┘ │ │
TARGET PHASE │ └───────────────────────┘ │
└─────────────────────────────┘
Phase 1: CAPTURING — document → body → container → button
Phase 2: TARGET — event fires on the button
Phase 3: BUBBLING — button → container → body → document
// Bubbling (default) — handler fires during bubble phase
document.querySelector('.container').addEventListener('click', (e) => {
console.log('Container clicked (bubbling)');
});
// Capturing — handler fires during capture phase
document.querySelector('.container').addEventListener('click', (e) => {
console.log('Container clicked (capturing)');
}, true); // or { capture: true }
// Stop propagation
button.addEventListener('click', (e) => {
e.stopPropagation(); // prevents further propagation
e.stopImmediatePropagation(); // also prevents other handlers on same element
});
// Prevent default behavior
const link = document.querySelector('a');
link.addEventListener('click', (e) => {
e.preventDefault(); // prevents navigation
console.log('Link click intercepted');
});

Event Delegation

Event delegation leverages bubbling to handle events on many elements with a single listener on a parent:

// BAD: individual listeners on each item
document.querySelectorAll('.list-item').forEach(item => {
item.addEventListener('click', handleItemClick); // 1000 listeners for 1000 items
});
// GOOD: single listener on the parent
document.querySelector('.list').addEventListener('click', (event) => {
// Find the closest .list-item ancestor of the clicked target
const item = event.target.closest('.list-item');
// Ignore clicks that are not on a list item
if (!item) return;
// Verify the item is inside our list (not a nested list)
if (!event.currentTarget.contains(item)) return;
console.log('Clicked item:', item.dataset.id);
});

Benefits of event delegation:

  • Fewer listeners — better memory usage
  • Dynamic elements — works for elements added after the listener is set
  • Simpler cleanup — remove one listener instead of many

Common DOM Events

CategoryEvents
Mouseclick, dblclick, mousedown, mouseup, mousemove, mouseenter, mouseleave
Keyboardkeydown, keyup, keypress (deprecated)
Formsubmit, change, input, focus, blur, invalid
DocumentDOMContentLoaded, load, beforeunload, visibilitychange
Scrollscroll, scrollend
Touchtouchstart, touchmove, touchend, touchcancel
Pointerpointerdown, pointermove, pointerup (unifies mouse + touch)
Dragdragstart, drag, dragover, drop, dragend
Clipboardcopy, cut, paste
Mediaplay, pause, ended, timeupdate
Animationanimationstart, animationend, transitionend

The Virtual DOM Concept

Frameworks like React introduced the Virtual DOM to optimize DOM updates. Instead of modifying the real DOM directly, changes are made to a lightweight in-memory representation, and only the minimal set of actual DOM operations is applied.

Traditional DOM Manipulation:
State Change → Directly mutate DOM → Browser reflow/repaint
(Every change triggers expensive DOM operations)
Virtual DOM Approach:
State Change → New Virtual DOM tree → Diff with previous → Minimal DOM patches
┌───────────────┴───────────────┐
│ Reconciliation │
│ (Diffing Algorithm) │
│ │
│ Old VDOM New VDOM │
│ ┌──────┐ ┌──────┐ │
│ │ div │ │ div │ │
│ │ ├─h1 │ │ ├─h1 │ │
│ │ ├─p │ ──▶ │ ├─p* │ ←── changed
│ │ └─ul │ │ ├─ul │ │
│ │ ├li│ │ │ ├li│ │
│ │ └li│ │ │ ├li│ │
│ └──────┘ │ │ └li│ ←── added
│ │ └────┘ │
│ │
│ Patch: update p text, │
│ append new li │
└───────────────────────────────┘

Why Not Just Use the Real DOM?

OperationCost
Reading DOM propertiesLow
Modifying attributes/stylesMedium
Adding/removing elementsMedium-High
Triggering layout (reflow)High
Painting pixelsHigh
Compositing layersMedium

The Virtual DOM batches changes and minimizes the number of expensive real DOM operations. However, it is not always faster than careful manual DOM manipulation — it trades peak performance for developer productivity.


Essential Web APIs

Fetch API

The Fetch API provides a modern interface for making HTTP requests:

// Basic GET request
async function getUsers() {
try {
const response = await fetch('https://api.example.com/users');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
}
const users = await response.json();
return users;
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request was cancelled');
} else {
console.error('Fetch failed:', error);
}
}
}
// POST with JSON body
async function createUser(userData) {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token,
},
body: JSON.stringify(userData),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
return response.json();
}
// With AbortController (timeout/cancellation)
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
return response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('Request timed out');
}
throw error;
}
}
// Upload with progress (using XMLHttpRequest — Fetch has no progress events)
function uploadWithProgress(file, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload');
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
onProgress(Math.round((event.loaded / event.total) * 100));
}
});
xhr.addEventListener('load', () => resolve(JSON.parse(xhr.response)));
xhr.addEventListener('error', () => reject(new Error('Upload failed')));
const formData = new FormData();
formData.append('file', file);
xhr.send(formData);
});
}

Web Storage API

// localStorage — persists across sessions
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme'); // 'dark'
localStorage.removeItem('theme');
localStorage.clear(); // remove everything
// Store complex data as JSON
const user = { name: 'Alice', preferences: { lang: 'en' } };
localStorage.setItem('user', JSON.stringify(user));
const saved = JSON.parse(localStorage.getItem('user'));
// sessionStorage — cleared when tab/window closes
sessionStorage.setItem('tempData', 'value');
// Storage event — fires in OTHER tabs/windows
window.addEventListener('storage', (event) => {
console.log('Key:', event.key);
console.log('Old value:', event.oldValue);
console.log('New value:', event.newValue);
console.log('URL:', event.url);
});

IntersectionObserver

Efficiently observe when elements enter or leave the viewport:

// Lazy-load images
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // load the real image
img.classList.add('loaded');
observer.unobserve(img); // stop watching this image
}
});
}, {
rootMargin: '200px', // start loading 200px before entering viewport
threshold: 0, // trigger as soon as any part is visible
});
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
// Infinite scroll
const sentinel = document.querySelector('#load-more-sentinel');
const scrollObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadMoreContent();
}
}, { threshold: 1.0 }); // fully visible
scrollObserver.observe(sentinel);
// Fade-in animation on scroll
const animObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
entry.target.classList.toggle('visible', entry.isIntersecting);
});
}, { threshold: 0.1 });
document.querySelectorAll('.animate-on-scroll').forEach(el => {
animObserver.observe(el);
});

Other Important Web APIs

// ResizeObserver — observe element size changes
const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
console.log('New size:', entry.contentRect.width, entry.contentRect.height);
}
});
resizeObserver.observe(document.querySelector('.responsive-widget'));
// MutationObserver — observe DOM changes
const mutObserver = new MutationObserver(mutations => {
mutations.forEach(m => {
if (m.type === 'childList') {
console.log('Children changed:', m.addedNodes, m.removedNodes);
}
if (m.type === 'attributes') {
console.log('Attribute changed:', m.attributeName);
}
});
});
mutObserver.observe(document.querySelector('#dynamic-content'), {
childList: true,
attributes: true,
subtree: true,
});
// Clipboard API
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
console.log('Copied!');
} catch (err) {
console.error('Failed to copy:', err);
}
}
// Geolocation API
navigator.geolocation.getCurrentPosition(
(position) => {
console.log('Lat:', position.coords.latitude);
console.log('Lng:', position.coords.longitude);
},
(error) => console.error('Geolocation error:', error),
{ enableHighAccuracy: true, timeout: 5000 }
);
// History API (SPA routing)
history.pushState({ page: 'about' }, '', '/about');
history.replaceState({ page: 'home' }, '', '/');
window.addEventListener('popstate', (event) => {
console.log('Navigation to:', event.state);
});

Practical Example: Building a Dynamic Todo List

Bringing together DOM manipulation, events, and delegation:

class TodoApp {
constructor(containerSelector) {
this.container = document.querySelector(containerSelector);
this.todos = JSON.parse(localStorage.getItem('todos')) || [];
this.render();
this.bindEvents();
}
bindEvents() {
// Event delegation on the list
this.container.addEventListener('click', (e) => {
const deleteBtn = e.target.closest('[data-action="delete"]');
if (deleteBtn) {
const id = Number(deleteBtn.closest('li').dataset.id);
this.deleteTodo(id);
return;
}
const checkbox = e.target.closest('input[type="checkbox"]');
if (checkbox) {
const id = Number(checkbox.closest('li').dataset.id);
this.toggleTodo(id);
}
});
// Form submission
this.container.querySelector('form').addEventListener('submit', (e) => {
e.preventDefault();
const input = e.target.querySelector('input[type="text"]');
const text = input.value.trim();
if (text) {
this.addTodo(text);
input.value = '';
input.focus();
}
});
}
addTodo(text) {
this.todos.push({
id: Date.now(),
text,
completed: false,
});
this.save();
this.render();
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
this.save();
this.render();
}
}
deleteTodo(id) {
this.todos = this.todos.filter(t => t.id !== id);
this.save();
this.render();
}
save() {
localStorage.setItem('todos', JSON.stringify(this.todos));
}
render() {
const list = this.container.querySelector('ul') ||
document.createElement('ul');
list.innerHTML = this.todos.map(todo => `
<li data-id="${todo.id}" class="${todo.completed ? 'completed' : ''}">
<label>
<input type="checkbox" ${todo.completed ? 'checked' : ''} />
<span>${this.escapeHtml(todo.text)}</span>
</label>
<button data-action="delete" aria-label="Delete">×</button>
</li>
`).join('');
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new TodoApp('#todo-app');
});

Performance Tips for DOM Operations

TipExplanation
Batch DOM reads and writesReading layout properties forces reflow. Read everything first, then write.
Use DocumentFragmentBuild a subtree in memory, insert once.
Debounce scroll/resize handlersAvoid firing expensive logic on every frame.
Use event delegationOne listener on a parent instead of many on children.
Prefer textContent over innerHTMLFaster and safer (no HTML parsing).
Use requestAnimationFrameSchedule visual updates for the next repaint.
Minimize forced synchronous layoutsAvoid reading layout properties (offsetHeight, getBoundingClientRect) after writes.
Detach elements before heavy modificationRemove from DOM, modify, re-insert.
// Debounce utility
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
window.addEventListener('resize', debounce(() => {
// expensive layout recalculation
}, 250));
// requestAnimationFrame for smooth animations
function animate() {
element.style.transform = `translateX(${position}px)`;
position += speed;
if (position < maxPosition) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);