DOM Basics

45 min read Intermediate

Learn to interact with web pages using the Document Object Model (DOM). Select elements, modify content, handle events, and create dynamic web experiences.

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.

HTML
<!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>
JavaScript
// 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.

JavaScript
// 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 }

Prefer querySelector

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.

JavaScript
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