The Document Object Model
The Document Object Model (DOM) is a programming interface that represents HTML documents as a tree structure of objects that JavaScript can access and manipulate. It serves as the bridge between web pages and programming languages, transforming static HTML into dynamic, interactive applications.
What the DOM Provides:
Structured Representation: HTML elements become JavaScript objects
API for Manipulation: Methods to find, create, modify, and delete elements
Event System: Mechanisms to respond to user interactions
Live Connection: Changes to the DOM immediately reflect in the browser
Platform Independence: Standard interface across all browsers

When a browser loads an HTML page, it parses the markup and creates a DOM tree in memory. Each HTML element becomes a node object with properties and methods. JavaScript can then access this tree through the global document object, which represents the entire page. Understanding that the DOM is a live representation—not the HTML source code—is crucial for effective web development.
The browser DOM APIs
The browser provides several related APIs for working with web pages:
DOM Core: Finding, accessing, and modifying elements and their content
DOM Events: Handling user interactions (clicks, keyboard input, form submissions)
DOM Style: Modifying CSS properties and classes dynamically
DOM Traversal: Navigating relationships between elements (parents, children, siblings)
DOM HTML: Specific methods and properties for HTML elements
DOM Manipulation: Creating, moving, and removing elements from the page
In essence: The HTML DOM is a standard for how to get, change, add, or delete HTML elements using JavaScript.
DOM Methods and Properties
DOM methods are actions you can perform on HTML elements (functions you can call). DOM properties are values associated with HTML elements that you can read or modify (like object properties).
html
<!DOCTYPE html>
<html>
<head>
<title>DOM Basics</title>
</head>
<body>
<h1 id="main-title">Original Title</h1>
<p id="demo"></p>
<script>
// getElementById is a METHOD - it performs an action
const element = document.getElementById("demo");
// innerHTML is a PROPERTY - it holds a value
element.innerHTML = "Hello World!";
// Chaining method and property access
document.getElementById("main-title").innerHTML = "New Title";
// Properties can be read and written
const currentText = element.innerHTML; // Read
element.innerHTML = "Updated text"; // Write
</script>
</body>
</html>Methods are called with parentheses and often accept arguments: getElementById("demo"). Properties are accessed without parentheses like regular object properties: element.innerHTML. Methods typically perform actions or return values, while properties store the current state of elements. Understanding this distinction helps you read documentation and write cleaner code.
Finding HTML elements
Before you can manipulate elements, you need to select them from the DOM. JavaScript provides multiple methods for finding elements, each suited to different scenarios.
Selecting by ID
// Select single element by unique ID
const header = document.getElementById("main-header");
// IDs should be unique - returns first match or null
const element = document.getElementById("unique-id");
if (element) {
console.log("Element found:", element);
} else {
console.log("Element not found");
}
// Common pattern: store references for reuse
const loginButton = document.getElementById("login-btn");
const errorMessage = document.getElementById("error-msg"); getElementById() returns a single element object or null if no match is found. ID selectors are the fastest selection method because IDs must be unique. Always check if the element exists before manipulating it to avoid "cannot read property of null" errors. This method only exists on the document object, not on individual elements.
Selecting by tag name
// Returns HTMLCollection (array-like, but not a true array)
const paragraphs = document.getElementsByTagName("p");
console.log(paragraphs.length); // Number of <p> elements
// Access by index (zero-based)
const firstParagraph = paragraphs[0];
const secondParagraph = paragraphs[1];
// Iterate with for loop
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.color = "blue";
}
// Convert to array for array methods
const paragraphArray = Array.from(paragraphs);
paragraphArray.forEach(p => {
console.log(p.textContent);
});
// Can be called on specific elements to search within them
const container = document.getElementById("content");
const linksInContainer = container.getElementsByTagName("a"); getElementsByTagName() returns an HTMLCollection, which is live—if the DOM changes, the collection updates automatically. Access elements by index like arrays, but note that HTMLCollections lack array methods like forEach(). Convert to a true array with Array.from() to use array methods. This method can be called on any element to search within its descendants, not just on the document.
Selecting by Class name
// Select all elements with a specific class
const highlights = document.getElementsByClassName("highlight");
// Multiple classes (elements must have all specified classes)
const featured = document.getElementsByClassName("card featured");
// HTMLCollection is live and updates automatically
console.log(highlights.length); // e.g., 5
const div = document.createElement("div");
div.className = "highlight";
document.body.appendChild(div);
console.log(highlights.length); // Now 6!
// Practical example: styling all cards
const cards = document.getElementsByClassName("card");
for (let i = 0; i < cards.length; i++) {
cards[i].style.borderRadius = "8px";
cards[i].style.boxShadow = "0 2px 4px rgba(0,0,0,0.1)";
}getElementsByClassName() returns a live HTMLCollection of all elements with the specified class name. Class names are case-sensitive. When searching for multiple classes (space-separated), elements must have all specified classes to match. Like getElementsByTagName(), this method can be called on any element to narrow the search scope to that element's descendants.
Selecting with CSS Selectors (recommended)
// querySelector - returns FIRST match or null
const firstButton = document.querySelector("button");
const mainNav = document.querySelector("#main-nav");
const firstCard = document.querySelector(".card");
const submitBtn = document.querySelector("button[type='submit']");
const firstLink = document.querySelector("nav a");
// querySelectorAll - returns NodeList of ALL matches
const allButtons = document.querySelectorAll("button");
const allCards = document.querySelectorAll(".card");
const navLinks = document.querySelectorAll("nav a");
// Complex CSS selectors work
const activeItems = document.querySelectorAll(".menu-item.active");
const evenRows = document.querySelectorAll("tr:nth-child(even)");
const checkedInputs = document.querySelectorAll("input:checked");
const externalLinks = document.querySelectorAll("a[href^='http']");
// NodeList supports forEach (unlike HTMLCollection)
allCards.forEach(card => {
card.addEventListener("click", handleCardClick);
});
// Template literal for dynamic selectors
const userId = "user-123";
const userElement = document.querySelector(`#${userId}`);
const userPosts = document.querySelectorAll(`[data-user="${userId}"]`);
// Scoped queries on specific elements
const sidebar = document.querySelector("#sidebar");
const sidebarLinks = sidebar.querySelectorAll("a"); querySelector() and querySelectorAll() are the modern, preferred methods for selecting elements. They accept any valid CSS selector, making them incredibly powerful and flexible. querySelector() returns the first match or null, while querySelectorAll() returns a NodeList of all matches (which is static, not live like HTMLCollection). NodeLists support forEach() directly, making iteration easier. These methods can use the full power of CSS selectors including pseudo-classes, attribute selectors, and combinators.
Comparison of Selection Methods
// Performance: ID is fastest
document.getElementById("unique"); // Fastest
// Flexibility: querySelector is most flexible
document.querySelector("#unique"); // Most flexible
document.querySelector(".class-name");
document.querySelector("div > p:first-child");
// Live vs Static collections
const liveCollection = document.getElementsByClassName("item");
const staticList = document.querySelectorAll(".item");
console.log(liveCollection.length); // e.g., 3
console.log(staticList.length); // 3
const newItem = document.createElement("div");
newItem.className = "item";
document.body.appendChild(newItem);
console.log(liveCollection.length); // 4 (updated automatically!)
console.log(staticList.length); // Still 3 (static snapshot)When to use each method:
getElementById(): When you have a unique ID and need maximum performance
getElementsByClassName() / getElementsByTagName(): When you need a live collection that updates automatically
querySelector(): When you need one element with flexible CSS selector matching
querySelectorAll(): Most common choice for selecting multiple elements with modern syntax
Be aware of live vs. static collections: getElementsBy* methods return live HTMLCollections that automatically update, while querySelectorAll() returns a static NodeList that captures a snapshot. Live collections can cause unexpected behavior if you're modifying the DOM while iterating
Manipulating element content
Once you've selected elements, you can read and modify their content using various properties.
innerHTML vs textContent vs innerText
const container = document.getElementById("content");
// innerHTML - Gets/sets HTML markup
container.innerHTML = "<p>This is <strong>HTML</strong></p>";
console.log(container.innerHTML);
// Output: <p>This is <strong>HTML</strong></p>
// textContent - Gets/sets text only (all markup stripped)
container.textContent = "<p>This is <strong>HTML</strong></p>";
console.log(container.textContent);
// Output: <p>This is <strong>HTML</strong></p> (displayed as plain text)
// innerText - Similar to textContent but respects styling (slower)
const hidden = document.getElementById("hidden-div");
hidden.style.display = "none";
hidden.innerHTML = "<p>Hidden content</p>";
console.log(hidden.textContent); // "<p>Hidden content</p>"
console.log(hidden.innerText); // "" (empty, because element is hidden)
// Reading existing content
const article = document.querySelector("article");
const htmlContent = article.innerHTML;
const textOnly = article.textContent;
// Practical example: updating a counter
const counter = document.getElementById("counter");
let count = parseInt(counter.textContent);
count++;
counter.textContent = count;Security Warning: Never use innerHTML with untrusted user input—it can execute malicious scripts (XSS attacks). If you need to insert user-provided text, use textContent instead, which treats everything as plain text. Use innerHTML only when you control the HTML content completely.
Performance: innerHTML is slower because the browser must parse HTML. textContent is faster for plain text. innerText is slowest because it considers CSS styling and triggers reflow. For most cases, prefer textContent for reading/writing plain text.
Modifying attributes
// Getting attributes
const link = document.querySelector("a");
const href = link.getAttribute("href");
const target = link.getAttribute("target");
// Setting attributes
link.setAttribute("href", "https://example.com");
link.setAttribute("target", "_blank");
link.setAttribute("rel", "noopener noreferrer");
// Removing attributes
link.removeAttribute("target");
// Checking if attribute exists
if (link.hasAttribute("href")) {
console.log("Link has href attribute");
}
// Direct property access (for standard attributes)
const img = document.querySelector("img");
img.src = "new-image.jpg";
img.alt = "Description";
img.width = 300;
// Data attributes (custom attributes)
const user = document.getElementById("user-card");
user.setAttribute("data-user-id", "12345");
user.setAttribute("data-role", "admin");
// Accessing data attributes via dataset property
console.log(user.dataset.userId); // "12345"
console.log(user.dataset.role); // "admin"
user.dataset.status = "active"; // Sets data-status="active"
// Class manipulation
const button = document.querySelector("button");
button.className = "btn btn-primary"; // Replace all classes
button.className += " btn-large"; // Add class (old way)Attributes can be manipulated using getAttribute(), setAttribute(), removeAttribute(), and hasAttribute() methods. For standard HTML attributes, you can also use direct property access (element.src, element.href). Data attributes (data-*) are perfect for storing custom information and can be accessed via the dataset property, which automatically converts data-user-id to dataset.userId using camelCase. Direct property access is generally faster and more convenient for standard attributes.
Working with Classes
const element = document.querySelector(".card");
// classList API (modern and preferred)
element.classList.add("active"); // Add single class
element.classList.add("featured", "new"); // Add multiple classes
element.classList.remove("old"); // Remove class
element.classList.toggle("expanded"); // Add if absent, remove if present
element.classList.replace("old", "new"); // Replace one class with another
// Check if class exists
if (element.classList.contains("active")) {
console.log("Element is active");
}
// Get number of classes
console.log(element.classList.length);
// Iterate over classes
element.classList.forEach(className => {
console.log(className);
});
// Practical example: toggle with callback
const button = document.getElementById("toggle-btn");
button.addEventListener("click", () => {
const sidebar = document.getElementById("sidebar");
const isExpanded = sidebar.classList.toggle("expanded");
button.textContent = isExpanded ? "Collapse" : "Expand";
});
// Old way (avoid)
element.className = "card active"; // Replaces all classes
element.className += " featured"; // Risky, can add duplicate spacesThe classList API is the modern, safe way to manipulate CSS classes. It prevents common pitfalls like duplicate classes or spacing issues. The toggle() method is particularly useful for showing/hiding elements or changing states—it returns true if the class was added and false if removed. Never manipulate className directly with string concatenation; use classList methods instead for reliability and readability.
Modifying CSS styles
JavaScript can modify element styles through the style property or by changing classes. Each approach has specific use cases.
Inline styles with style property
const box = document.getElementById("box");
// Setting individual CSS properties
box.style.color = "red";
box.style.backgroundColor = "yellow"; // Note: camelCase for hyphenated properties
box.style.fontSize = "20px";
box.style.marginTop = "10px";
box.style.borderRadius = "8px";
// CSS property names: kebab-case → camelCase
// background-color → backgroundColor
// font-size → fontSize
// margin-top → marginTop
// Reading computed styles (won't work with style property alone)
console.log(box.style.color); // Only returns inline styles!
// Get computed styles (includes CSS file styles)
const computedStyle = window.getComputedStyle(box);
console.log(computedStyle.color); // Full computed value
console.log(computedStyle.backgroundColor);
console.log(computedStyle.fontSize);
// Setting multiple styles at once with cssText
box.style.cssText = "color: blue; background: white; padding: 20px;";
// Or using template literals for readability
box.style.cssText = `
color: blue;
background-color: white;
padding: 20px;
border: 1px solid #ccc;
`;
// Removing inline styles
box.style.color = ""; // Removes inline color style
// Practical example: interactive button
const button = document.querySelector("button");
button.addEventListener("mouseenter", () => {
button.style.backgroundColor = "#007bff";
button.style.transform = "translateY(-2px)";
button.style.boxShadow = "0 4px 8px rgba(0,0,0,0.2)";
});
button.addEventListener("mouseleave", () => {
button.style.backgroundColor = "";
button.style.transform = "";
button.style.boxShadow = "";
});The style property only accesses inline styles set via JavaScript or the HTML style attribute. It won't return styles from CSS files or <style> tags. Use window.getComputedStyle(element) to read the final computed styles from all sources.
Best Practice: Prefer adding/removing classes over setting inline styles when possible. Inline styles have high specificity and are harder to override, making CSS maintenance difficult. Use inline styles only for dynamic values that can't be predefined in CSS (like animation positions or calculated dimensions).
Class-based styling (recommended)
<!DOCTYPE html>
<html>
<head>
<style>
.card {
background: white;
padding: 20px;
border-radius: 8px;
transition: all 0.3s;
}
.card.highlighted {
background: #fffbcc;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
transform: scale(1.05);
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="card" id="myCard">Card Content</div>
<script>
const card = document.getElementById("myCard");
// Toggle state with classes
card.addEventListener("click", () => {
card.classList.toggle("highlighted");
});
// Show/hide with classes (preferred over style.display)
function hideCard() {
card.classList.add("hidden");
}
function showCard() {
card.classList.remove("hidden");
}
// This approach is better because:
// 1. CSS stays in CSS files
// 2. Easier to maintain and update styles
// 3. Can leverage CSS transitions and animations
// 4. Better separation of concerns
</script>
</body>
</html>Using classes to control styling is the professional approach: define visual states in CSS, then use JavaScript to toggle between states by adding/removing classes. This separates concerns—CSS handles presentation, JavaScript handles behavior. Classes also enable CSS transitions and animations, which don't work well with directly manipulated inline styles. Reserve inline styles for dynamic values that must be calculated at runtime (positions, dimensions based on user input).
Creating and removing elements
The DOM isn't static—you can dynamically create new elements and remove existing ones to build interactive interfaces.
Creating new elements
// Create element
const newDiv = document.createElement("div");
// Set properties and content
newDiv.id = "dynamic-content";
newDiv.className = "card featured";
newDiv.textContent = "This is dynamically created!";
// Set attributes
newDiv.setAttribute("data-created", Date.now());
// Add styles
newDiv.style.padding = "20px";
newDiv.style.margin = "10px";
// Append to document
document.body.appendChild(newDiv);
// Create more complex structures
const article = document.createElement("article");
article.className = "blog-post";
const title = document.createElement("h2");
title.textContent = "My Blog Post";
article.appendChild(title);
const paragraph = document.createElement("p");
paragraph.textContent = "This is the post content.";
article.appendChild(paragraph);
const link = document.createElement("a");
link.href = "#";
link.textContent = "Read more";
article.appendChild(link);
document.body.appendChild(article);
// Creating elements from HTML strings (use with caution)
const container = document.getElementById("container");
container.innerHTML += "<div class='card'>New Card</div>"; // Replaces all event listeners!
// Better: insertAdjacentHTML (doesn't replace existing content)
container.insertAdjacentHTML("beforeend", "<div class='card'>New Card</div>");Creating elements with createElement() and building them programmatically is safer than using innerHTML with string concatenation, especially with user input. When you set innerHTML, all existing content is destroyed and recreated, removing event listeners. Use insertAdjacentHTML() when you need to insert HTML strings—it's faster and doesn't destroy existing content. For complex structures, consider using template literals or document fragments for better performance.
Inserting elements at specific positions
const parent = document.getElementById("parent");
const newChild = document.createElement("div");
// insertBefore - Insert before a reference element
const referenceElement = document.getElementById("middle-child");
parent.insertBefore(newChild, referenceElement);
// insertAdjacentElement - More flexible positioning
const target = document.getElementById("target");
const element = document.createElement("div");
target.insertAdjacentElement("beforebegin", element); // Before target
target.insertAdjacentElement("afterbegin", element); // First child of target
target.insertAdjacentElement("beforeend", element); // Last child of target
target.insertAdjacentElement("afterend", element); // After target
// insertAdjacentHTML - Insert HTML string
target.insertAdjacentHTML("beforeend", "<p>New paragraph</p>");
// insertAdjacentText - Insert text safely
target.insertAdjacentText("beforeend", "<script>alert('XSS')</script>"); // Safe! Displayed as text
// Practical example: adding list items
const list = document.getElementById("todo-list");
function addTodoItem(text) {
const li = document.createElement("li");
li.textContent = text;
li.className = "todo-item";
list.appendChild(li); // Or list.insertAdjacentElement("beforeend", li);
}
addTodoItem("Buy groceries");
addTodoItem("Walk the dog");insertAdjacentElement() and its siblings provide precise control over where new elements appear: beforebegin inserts before the element as a sibling, afterbegin inserts as the first child, beforeend inserts as the last child, and afterend inserts after the element as a sibling. These methods are more flexible than appendChild() which always adds at the end. Use insertAdjacentText() to safely insert user-provided text that should not be interpreted as HTML.
Removing elements
// Modern method: remove() - removes element from DOM
const element = document.getElementById("remove-me");
element.remove(); // Simple and direct
// Old method: removeChild() - requires parent reference
const parent = document.getElementById("parent");
const child = document.getElementById("child");
parent.removeChild(child);
// Removing all children
const container = document.getElementById("container");
// Method 1: innerHTML (fast but removes event listeners)
container.innerHTML = "";
// Method 2: Loop with removeChild (preserves memory)
while (container.firstChild) {
container.removeChild(container.firstChild);
}
// Method 3: Modern approach with remove()
while (container.firstChild) {
container.firstChild.remove();
}
// Practical example: removing completed todos
const todoList = document.getElementById("todo-list");
const completedItems = todoList.querySelectorAll(".completed");
completedItems.forEach(item => {
item.remove();
});
// Conditional removal
const cards = document.querySelectorAll(".card");
cards.forEach(card => {
if (card.dataset.status === "expired") {
card.remove();
}
});The modern remove() method is the simplest way to remove elements—just call it on the element you want to remove. The older removeChild() method requires a parent reference and is more verbose. When removing all children, using innerHTML = "" is fastest but doesn't properly clean up event listeners, potentially causing memory leaks. Using a loop with remove() or removeChild() is safer for elements with attached event listeners.
Cloning elements
// Clone an element and its descendants
const original = document.getElementById("template");
// Shallow clone (element only, no children)
const shallowClone = original.cloneNode(false);
// Deep clone (element and all descendants)
const deepClone = original.cloneNode(true);
// Clones don't include event listeners
document.body.appendChild(deepClone);
// Practical example: template pattern
const cardTemplate = document.getElementById("card-template");
function createCard(title, content) {
const card = cardTemplate.cloneNode(true);
card.id = ""; // Remove template ID
card.querySelector(".card-title").textContent = title;
card.querySelector(".card-content").textContent = content;
card.classList.remove("hidden");
return card;
}
const newCard = createCard("New Card", "Card content here");
document.getElementById("container").appendChild(newCard);cloneNode(true) creates a deep copy of an element including all descendants, while cloneNode(false) only copies the element itself. Clones are independent copies—changes to the clone don't affect the original. Event listeners are NOT copied, so you must reattach them. The template pattern is perfect for creating multiple similar elements: keep a hidden template in your HTML, clone it when needed, customize the clone, and add it to the page.
Handling events
Events are actions or occurrences that happen in the browser—clicks, key presses, form submissions, page loads, and more. JavaScript can listen for these events and respond accordingly.
Common event types
// Mouse events
element.addEventListener("click", handler); // Single click
element.addEventListener("dblclick", handler); // Double click
element.addEventListener("mousedown", handler); // Mouse button pressed
element.addEventListener("mouseup", handler); // Mouse button released
element.addEventListener("mouseenter", handler); // Mouse enters element
element.addEventListener("mouseleave", handler); // Mouse leaves element
element.addEventListener("mousemove", handler); // Mouse moves within element
element.addEventListener("mouseover", handler); // Mouse enters (bubbles)
element.addEventListener("mouseout", handler); // Mouse leaves (bubbles)
// Keyboard events
element.addEventListener("keydown", handler); // Key pressed
element.addEventListener("keyup", handler); // Key released
element.addEventListener("keypress", handler); // Key pressed (deprecated)
// Form events
element.addEventListener("submit", handler); // Form submitted
element.addEventListener("change", handler); // Input value changed (after blur)
element.addEventListener("input", handler); // Input value changing (real-time)
element.addEventListener("focus", handler); // Element receives focus
element.addEventListener("blur", handler); // Element loses focus
// Window/Document events
window.addEventListener("load", handler); // Page fully loaded
window.addEventListener("DOMContentLoaded", handler); // DOM ready (faster)
window.addEventListener("resize", handler); // Window resized
window.addEventListener("scroll", handler); // Page scrolled
window.addEventListener("beforeunload", handler); // Before page unload{% hint style="success" %} Understanding event types helps you respond to the right user actions. Mouse events handle pointer interactions, keyboard events capture typing, form events monitor user input, and window events track page state. DOMContentLoaded fires when the HTML is parsed and DOM is ready, while load waits for all resources (images, stylesheets). Use input for real-time validation and change for actions that should occur after the user finishes editing. {% endhint %}
Adding event listeners
const button = document.getElementById("myBtn");
// Modern approach: addEventListener (recommended)
button.addEventListener("click", function() {
console.log("Button clicked!");
});
// With arrow function
button.addEventListener("click", () => {
console.log("Button clicked!");
});
// Named function (can be removed later)
function handleClick() {
console.log("Button clicked!");
}
button.addEventListener("click", handleClick);
// Removing event listener (must use same function reference)
button.removeEventListener("click", handleClick);
// Old approaches (avoid - can only attach one handler)
button.onclick = function() {
console.log("Clicked");
};
// Inline HTML (worst - avoid entirely)
// <button onclick="handleClick()">Click</button>
// Multiple listeners on same element
button.addEventListener("click", handler1);
button.addEventListener("click", handler2); // Both will execute
// Event listener with options
button.addEventListener("click", handleClick, {
once: true, // Remove after first trigger
capture: false, // Use bubbling phase (default)
passive: true // Improve scroll performance
});
// Practical example: form validation
const form = document.getElementById("signup-form");
const emailInput = document.getElementById("email");
emailInput.addEventListener("input", () => {
const email = emailInput.value;
const isValid = email.includes("@");
emailInput.classList.toggle("invalid", !isValid);
});
form.addEventListener("submit", (event) => {
event.preventDefault(); // Prevent form submission
const formData = new FormData(form);
console.log("Form submitted:", Object.fromEntries(formData));
});Always use addEventListener() for attaching event handlers—it's the modern standard. Never use inline event handlers (onclick="...") in HTML as they mix structure with behavior and create security risks. The onclick property can only hold one function and will overwrite previous handlers. addEventListener() allows multiple handlers on the same element and provides better control with options like once, capture, and passive. Always use named functions if you need to remove listeners later, as anonymous functions can't be removed.
The Event Object
// Every event handler receives an event object parameter
button.addEventListener("click", function(event) {
// Common event properties
console.log(event.type); // Event type: "click"
console.log(event.target); // Element that triggered the event
console.log(event.currentTarget); // Element that has the listener
console.log(event.timeStamp); // Time event occurred
// Mouse event properties
console.log(event.clientX); // Mouse X position (viewport)
console.log(event.clientY); // Mouse Y position (viewport)
console.log(event.pageX); // Mouse X position (document)
console.log(event.pageY); // Mouse Y position (document)
console.log(event.button); // Which mouse button clicked
// Keyboard event properties
console.log(event.key); // Key value: "a", "Enter", "Escape"
console.log(event.code); // Physical key: "KeyA", "Enter"
console.log(event.ctrlKey); // Ctrl key pressed?
console.log(event.shiftKey); // Shift key pressed?
console.log(event.altKey); // Alt key pressed?
// Event control methods
event.preventDefault(); // Prevent default behavior
event.stopPropagation(); // Stop event bubbling
event.stopImmediatePropagation(); // Stop other handlers on this element
});
// Practical example: keyboard shortcuts
document.addEventListener("keydown", (e) => {
// Ctrl+S to save
if (e.ctrlKey && e.key === "s") {
e.preventDefault(); // Don't open browser save dialog
saveDocument();
}
// Escape to close modal
if (e.key === "Escape") {
closeModal();
}
});
// Practical example: drag and drop
let isDragging = false;
let startX, startY;
element.addEventListener("mousedown", (e) => {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
});
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
element.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
});
document.addEventListener("mouseup", () => {
isDragging = false;
});
// Form example: accessing input value
const input = document.querySelector("#username");
input.addEventListener("input", (e) => {
const newValue = e.target.value; // Current input value
console.log("User typed:", newValue);
});The event object contains valuable information about what happened. event.target is the element that triggered the event (the clicked button), while event.currentTarget is the element with the listener attached (useful in event delegation). Use preventDefault() to stop default behaviors like form submissions or link navigation. Use stopPropagation() to prevent events from bubbling up to parent elements. Keyboard events provide key (character value) and code (physical key), plus modifier key booleans for building keyboard shortcuts.
DOM Traversal
Navigating the DOM tree to find related elements is called DOM traversal.
Parent, Child, and Sibling relationships
const element = document.getElementById("middle");
// Parent relationships
element.parentNode; // Immediate parent
element.parentElement; // Immediate parent (element only)
element.closest(".container"); // Nearest ancestor matching selector
// Child relationships
element.children; // HTMLCollection of element children
element.childNodes; // NodeList of all child nodes (includes text nodes)
element.firstElementChild; // First child element
element.lastElementChild; // Last child element
element.firstChild; // First child node
element.lastChild; // Last child node
element.childElementCount; // Number of child elements
// Sibling relationships
element.nextElementSibling; // Next sibling element
element.previousElementSibling; // Previous sibling element
element.nextSibling; // Next sibling node
element.previousSibling; // Previous sibling node
// Practical example: find closest form
const input = document.querySelector("input[name='email']");
const form = input.closest("form");
form.addEventListener("submit", handleSubmit);
// Example: highlight siblings
const item = document.querySelector(".active");
let sibling = item.nextElementSibling;
while (sibling) {
sibling.classList.add("nearby");
sibling = sibling.nextElementSibling;
}
// Example: remove all children except first
const list = document.getElementById("list");
while (list.children.length > 1) {
list.lastElementChild.remove();
}{% hint style="warning" %} childNodes and children are different: childNodes includes all nodes (text, comments, elements) while children only includes elements. Similarly, firstChild might be a text node, while firstElementChild is always an element. Text nodes are created by whitespace in HTML, which can cause unexpected behavior. For most purposes, use the element-specific properties (children, firstElementChild, etc.) to avoid dealing with text nodes. {% endhint %}
Querying within elements
// Search within a specific element
const sidebar = document.getElementById("sidebar");
// Query methods work on any element, not just document
const sidebarLinks = sidebar.querySelectorAll("a");
const sidebarHeadings = sidebar.getElementsByTagName("h3");
// Scoped searches
const containers = document.querySelectorAll(".container");
containers.forEach(container => {
const buttons = container.querySelectorAll("button");
console.log(`Container has ${buttons.length} buttons`);
});
// Practical: find and highlight parent section
const errorMessage = document.querySelector(".error");
if (errorMessage) {
const section = errorMessage.closest("section");
section.classList.add("has-error");
}
// Navigate up and down the tree
const cell = document.querySelector("td");
const row = cell.parentElement; // <tr>
const table = row.closest("table"); // <table>
const allCellsInRow = row.children; // All <td> in this rowAll query methods (querySelector, querySelectorAll, getElementsBy*) can be called on any element, not just document. This scopes the search to descendants of that element only. The closest() method searches up the tree for the nearest ancestor matching a selector, which is perfect for finding container elements. These scoped searches are more efficient than searching the entire document and make code more maintainable by clearly expressing intent.
Best practices
1. Cache DOM References
// Bad - queries DOM repeatedly
for (let i = 0; i < 100; i++) {
document.getElementById("counter").textContent = i; // 100 DOM queries!
}
// Good - cache the reference
const counter = document.getElementById("counter");
for (let i = 0; i < 100; i++) {
counter.textContent = i; // 1 DOM query, 100 updates
}
// Cache collections
const buttons = document.querySelectorAll(".button"); // Cache once
buttons.forEach(button => {
button.addEventListener("click", handleClick);
});2. Minimize reflows and repaints
// Bad - causes 3 separate reflows
element.style.width = "100px";
element.style.height = "100px";
element.style.margin = "10px";
// Good - single reflow with cssText
element.style.cssText = "width: 100px; height: 100px; margin: 10px;";
// Even better - use classes
element.classList.add("large-box"); // All styles in one CSS rule
// Bad - reading layout properties triggers reflow
for (let i = 0; i < elements.length; i++) {
const height = elements[i].offsetHeight; // Forces reflow each iteration
elements[i].style.marginTop = (height / 2) + "px";
}
// Good - batch reads and writes
const heights = [];
// Read phase
for (let i = 0; i < elements.length; i++) {
heights.push(elements[i].offsetHeight);
}
// Write phase
for (let i = 0; i < elements.length; i++) {
elements[i].style.marginTop = (heights[i] / 2) + "px";
}3. Use document fragments for multiple elements
// Bad - adds elements one by one (multiple reflows)
const list = document.getElementById("list");
for (let i = 0; i < 100; i++) {
const li = document.createElement("li");
li.textContent = `Item ${i}`;
list.appendChild(li); // Reflow after each append!
}
// Good - use document fragment
const list = document.getElementById("list");
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const li = document.createElement("li");
li.textContent = `Item ${i}`;
fragment.appendChild(li); // No reflow
}
list.appendChild(fragment); // Single reflow for all 100 items4. Remove Event Listeners when not needed
// Memory leak - listeners never removed
function attachListeners() {
const buttons = document.querySelectorAll(".temp-button");
buttons.forEach(button => {
button.addEventListener("click", handleClick);
});
// If buttons are removed but listeners aren't, memory leak!
}
// Good - clean up listeners
function attachListeners() {
const buttons = document.querySelectorAll(".temp-button");
function cleanup() {
buttons.forEach(button => {
button.removeEventListener("click", handleClick);
});
}
buttons.forEach(button => {
button.addEventListener("click", handleClick);
});
return cleanup; // Caller can remove listeners when done
}
const cleanup = attachListeners();
// Later...
cleanup(); // Remove all listeners
// Or use event delegation to avoid many listeners entirely5. Prefer semantic HTML and CSS over JavaScript
// Bad - JavaScript for purely visual effects
button.addEventListener("click", () => {
if (menu.style.display === "none") {
menu.style.display = "block";
} else {
menu.style.display = "none";
}
});
// Good - toggle class, let CSS handle visual transition
button.addEventListener("click", () => {
menu.classList.toggle("open");
});
// CSS handles the rest:
// .menu { display: none; transition: all 0.3s; }
// .menu.open { display: block; }{% hint style="success" %} Efficient DOM manipulation requires understanding browser rendering: reading layout properties like offsetHeight triggers reflows (expensive), and multiple DOM changes cause multiple repaints. Cache element references to avoid repeated queries. Batch DOM changes together, or use Document Fragments to build complex structures off-DOM then add them in one operation. Clean up event listeners to prevent memory leaks, especially with dynamically created elements. Let CSS handle styling and animations—JavaScript should manage state (add/remove classes), not visual details. {% endhint %}
Modern DOM APIs
IntersectionObserver
// Detect when elements enter viewport (lazy loading, infinite scroll)
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Element is visible
const img = entry.target;
img.src = img.dataset.src; // Load image
observer.unobserve(img); // Stop observing
}
});
}, {
threshold: 0.5, // Trigger when 50% visible
rootMargin: "100px" // Start loading 100px before visible
});
// Observe all images
const lazyImages = document.querySelectorAll("img[data-src]");
lazyImages.forEach(img => observer.observe(img));MutationObserver
// Watch for DOM changes
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
console.log("DOM changed:", mutation.type);
if (mutation.type === "childList") {
console.log("Nodes added:", mutation.addedNodes);
console.log("Nodes removed:", mutation.removedNodes);
}
if (mutation.type === "attributes") {
console.log("Attribute changed:", mutation.attributeName);
}
});
});
// Start observing
const target = document.getElementById("container");
observer.observe(target, {
childList: true, // Watch for added/removed children
attributes: true, // Watch for attribute changes
subtree: true, // Watch all descendants
attributeOldValue: true // Record old attribute values
});
// Stop observing
observer.disconnect();ResizeObserver
// Detect element size changes
const observer = new ResizeObserver(entries => {
entries.forEach(entry => {
const { width, height } = entry.contentRect;
console.log(`Element resized to ${width}x${height}`);
// Adjust layout based on size
if (width < 600) {
entry.target.classList.add("mobile-layout");
}
});
});
const element = document.getElementById("responsive-content");
observer.observe(element);Modern Observer APIs provide efficient ways to monitor changes without polling. IntersectionObserver detects when elements enter/exit the viewport, perfect for lazy loading and infinite scroll. MutationObserver watches for DOM structure or attribute changes, useful for frameworks or plugins that need to react to DOM modifications. ResizeObserver tracks element size changes, better than window resize events for responsive components. These APIs are more performant than traditional polling approaches
Last updated
Was this helpful?