Back to 33 Js Concepts

DOM Manipulation

docs/concepts/dom.mdx

latest82.7 KB
Original Source

How does JavaScript change what you see on a webpage? How do you click a button and see new content appear, or type in a form and watch suggestions pop up? How does a "dark mode" toggle instantly transform an entire page?

javascript
// The DOM lets you do things like this:
document.querySelector('h1').textContent = 'Hello, DOM!'
document.body.style.backgroundColor = 'lightblue'
document.getElementById('btn').addEventListener('click', handleClick)

The Document Object Model (DOM) is the bridge between your HTML and JavaScript. It lets you read, modify, and respond to changes in web page content. With the DOM, you can use methods like querySelector() to find elements, getElementById() to grab specific nodes, and addEventListener() to respond to user interactions.

<Info> **What you'll learn in this guide:** - What the DOM is in JavaScript and how it differs from HTML - How to select DOM elements (getElementById vs querySelector) - How to traverse the DOM tree (parent, children, siblings) - How to manipulate DOM elements (create, modify, remove) - The difference between properties and attributes - How the browser turns DOM → pixels (the Critical Rendering Path) - Performance best practices (avoid layout thrashing!) </Info> <Warning> **Prerequisite:** This guide assumes basic familiarity with [HTML](https://developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML) and [CSS](https://developer.mozilla.org/en-US/docs/Learn/CSS/First_steps). If you're new to web development, start there first! </Warning>

What is the DOM in JavaScript?

The Document Object Model (DOM) is a programming interface that represents HTML documents as a tree of objects. As specified by the WHATWG DOM Living Standard, when a browser loads a webpage, it parses the HTML and creates the DOM, a live, structured representation that JavaScript can read and modify. Every element, attribute, and piece of text becomes a node in this tree. In short: the DOM is how JavaScript "sees" and changes a webpage.


How the DOM Tree Structure Works

Think of the DOM like a family tree. At the top sits document (the family historian who knows everyone). Below it is <html> (the matriarch), which has two children: <head> and <body>. Each of these has their own children, grandchildren, and so on.

                           THE DOM FAMILY TREE

                              ┌──────────┐
                              │ document │  ← The family historian
                              │ (root)   │    (knows everyone!)
                              └────┬─────┘
                                   │
                              ┌────┴─────┐
                              │  <html>  │  ← Great-grandma
                              └────┬─────┘    (the matriarch)
                     ┌─────────────┴─────────────┐
                     │                           │
                ┌────┴────┐                 ┌────┴────┐
                │ <head>  │                 │ <body>  │  ← The two branches
                └────┬────┘                 └────┬────┘    of the family
                     │                           │
              ┌──────┴──────┐         ┌──────────┼──────────┐
              │             │         │          │          │
         ┌────┴────┐  ┌────┴────┐ ┌───┴───┐ ┌────┴────┐ ┌───┴───┐
         │ <title> │  │ <meta>  │ │ <nav> │ │ <main>  │ │<footer>│
         └────┬────┘  └─────────┘ └───┬───┘ └────┬────┘ └───────┘
              │                       │          │
         "My Page"               ┌────┴────┐  ┌──┴──┐
          (text)                 │  <ul>   │  │<div>│  ← Cousins
                                 └────┬────┘  └──┬──┘
                                      │          │
                                 ┌────┼────┐    ...
                                 │    │    │
                               <li> <li> <li>   ← Siblings

Just like navigating a family reunion, the DOM lets you:

ActionFamily AnalogyDOM Method
Find your parent"Who's your mom?"element.parentNode
Find your kids"Where are your children?"element.children
Find your sibling"Who's your brother?"element.nextElementSibling
Search the whole family"Where's cousin Bob?"document.querySelector('#bob')
<Note> **Key insight:** Every element, text, and comment in your HTML becomes a "node" in this tree. JavaScript lets you navigate this tree and modify it: changing content, adding elements, or removing them entirely. </Note>

What the DOM is NOT

The DOM is NOT Your HTML Source Code

Here's the key thing: your HTML file and the DOM are different things:

<Tabs> <Tab title="HTML Source"> ```html <!-- What you wrote (invalid HTML - missing head/body) --> <!DOCTYPE html> <html> Hello, World! </html> ``` </Tab> <Tab title="Resulting DOM"> ```html <!-- What the browser creates (fixed!) --> <!DOCTYPE html> <html> <head></head> <body> Hello, World! </body> </html> ``` </Tab> </Tabs>

The browser fixes your mistakes! It adds missing <head> and <body> tags, closes unclosed tags, and corrects nesting errors. The DOM is the corrected version. According to the HTML specification's parsing algorithm, browsers must follow specific error-recovery rules to handle malformed markup consistently across implementations.

The DOM is NOT What You See in DevTools (Exactly)

DevTools shows you something close to the DOM, but it also shows CSS pseudo-elements (::before, ::after) which are NOT part of the DOM:

css
/* This creates visual content, but NOT DOM nodes */
.quote::before {
  content: '"';
}

Pseudo-elements exist in the render tree (for display), but not in the DOM (for JavaScript). You can't select them with querySelector!

The DOM is NOT the Render Tree

The Render Tree is what actually gets painted to the screen. It excludes:

html
<!-- These are in the DOM but NOT in the Render Tree -->
<head>...</head>                    <!-- Never rendered -->
<script>...</script>                <!-- Never rendered -->
<div style="display: none">Hidden</div>  <!-- Excluded from render -->
DOM                          Render Tree
┌─────────────────────┐     ┌─────────────────────┐
│ <html>              │     │ <html>              │
│   <head>            │     │   <body>            │
│     <title>         │     │     <h1>            │
│   <body>            │     │       "Hello"       │
│     <h1>Hello</h1>  │     │     <p>             │
│     <p>World</p>    │     │       "World"       │
│     <div hidden>    │     │                     │
│       Secret!       │     │  (no hidden div!)   │
│     </div>          │     │                     │
└─────────────────────┘     └─────────────────────┘

The document Object: Your Entry Point

The document object is your gateway to the DOM. It's automatically available in any browser JavaScript. Key properties include document.documentElement (the root <html> element), document.head, document.body, and document.title:

javascript
// document is the root of everything
console.log(document)                    // The entire document
console.log(document.documentElement)    // <html> element
console.log(document.head)               // <head> element
console.log(document.body)               // <body> element
console.log(document.title)              // Page title (getter/setter!)

// You can modify the document
document.title = 'New Title'             // Changes browser tab title

DOM Node Types Explained

Everything in the DOM is a Node. But not all nodes are created equal!

The Node Type Hierarchy

                            Node (base class)
                              │
        ┌─────────────────────┼─────────────────────┐
        │                     │                     │
    Document             Element              CharacterData
        │                     │                     │
   HTMLDocument          ┌────┴────┐         ┌─────┴─────┐
                         │         │         │           │
                   HTMLElement  SVGElement  Text      Comment
                         │
        ┌────────────────┼────────────────┐
        │                │                │
  HTMLDivElement  HTMLSpanElement  HTMLInputElement
                                        ...

Node Types You'll Encounter

Node TypenodeTypenodeNameExample
Element1Tag name (uppercase)<div>, <p>, <span>
Text3#textText inside elements
Comment8#comment<!-- comment -->
Document9#documentThe document object
DocumentFragment11#document-fragmentVirtual container
javascript
const div = document.createElement('div')
console.log(div.nodeType)   // 1 (Element)
console.log(div.nodeName)   // "DIV"

const text = document.createTextNode('Hello')
console.log(text.nodeType)  // 3 (Text)
console.log(text.nodeName)  // "#text"

console.log(document.nodeType)  // 9 (Document)
console.log(document.nodeName)  // "#document"

The createElement() and createTextNode() methods create new nodes that you can add to the DOM.

Node Type Constants

Instead of remembering numbers, use the constants:

javascript
Node.ELEMENT_NODE        // 1
Node.TEXT_NODE           // 3
Node.COMMENT_NODE        // 8
Node.DOCUMENT_NODE       // 9
Node.DOCUMENT_FRAGMENT_NODE  // 11

// Check if something is an element
if (node.nodeType === Node.ELEMENT_NODE) {
  console.log('This is an element!')
}

Visualizing a Real DOM Tree

Given this HTML:

html
<div id="container">
  <h1>Title</h1>
  <!-- A comment -->
  <p>Paragraph</p>
</div>

The actual DOM tree looks like this (including text nodes from whitespace!):

div#container
├── #text (newline + spaces)
├── h1
│   └── #text "Title"
├── #text (newline + spaces)
├── #comment " A comment "
├── #text (newline + spaces)
├── p
│   └── #text "Paragraph"
└── #text (newline)
<Warning> **The Whitespace Gotcha!** Line breaks and spaces between HTML tags create **text nodes**. This surprises many developers! We'll see how to handle this in the traversal section. </Warning>

How to Select DOM Elements

Before you can manipulate an element, you need to find it. JavaScript provides several methods through the document object:

The getElementById() Classic

The getElementById() method is the fastest way to select a single element by its unique ID:

javascript
// HTML: <div id="hero">Welcome!</div>

const hero = document.getElementById('hero')
console.log(hero)        // <div id="hero">Welcome!</div>
console.log(hero.id)     // "hero"
console.log(hero.textContent)  // "Welcome!"

// Returns null if not found (not an error!)
const ghost = document.getElementById('nonexistent')
console.log(ghost)  // null
<Tip> IDs must be unique in a document. If you have duplicate IDs, `getElementById` returns the first one. But don't do this. It's invalid HTML! </Tip>

getElementsByClassName() and getElementsByTagName()

getElementsByClassName() and getElementsByTagName() select multiple elements by class or tag name:

javascript
// HTML: 
// <p class="intro">First</p>
// <p class="intro">Second</p>
// <p>Third</p>

const intros = document.getElementsByClassName('intro')
console.log(intros.length)      // 2
console.log(intros[0])          // <p class="intro">First</p>
console.log(intros[0].textContent)  // "First"

const allParagraphs = document.getElementsByTagName('p')
console.log(allParagraphs.length)  // 3

The Modern Way: querySelector() and querySelectorAll()

querySelector() and querySelectorAll() use CSS selectors to find elements. Much more powerful!

javascript
// querySelector returns the FIRST match (or null)
const firstButton = document.querySelector('button')      // First <button> element
const submitBtn = document.querySelector('#submit')       // Element with id="submit"
const firstCard = document.querySelector('.card')         // First element with class="card"
const navLink = document.querySelector('nav a.active')    // <a class="active"> inside <nav>
const dataItem = document.querySelector('[data-id="123"]')  // Element with data-id="123"

// querySelectorAll returns ALL matches (NodeList)
const allButtons = document.querySelectorAll('button')    // All <button> elements
const allCards = document.querySelectorAll('.card')       // All elements with class="card"
const evenRows = document.querySelectorAll('tr:nth-child(even)')  // Every even table row

Selector Examples

javascript
// By ID
document.querySelector('#main')

// By class
document.querySelector('.active')
document.querySelectorAll('.btn.primary')

// By tag
document.querySelector('header')
document.querySelectorAll('li')

// By attribute
document.querySelector('[type="submit"]')
document.querySelector('[data-modal="login"]')

// Descendant selectors
document.querySelector('nav ul li a')
document.querySelector('.sidebar .widget:first-child')

// Pseudo-selectors (limited support)
document.querySelectorAll('input:not([type="hidden"])')
document.querySelector('p:first-of-type')

Live vs Static Collections

This difference trips up many developers. getElementsByClassName() returns a live HTMLCollection, while querySelectorAll() returns a static NodeList:

javascript
const liveList = document.getElementsByClassName('item')    // LIVE HTMLCollection
const staticList = document.querySelectorAll('.item')       // STATIC NodeList

// Start with 3 items
console.log(liveList.length)    // 3
console.log(staticList.length)  // 3

// Add a new item to the DOM
const newItem = document.createElement('div')
newItem.className = 'item'
document.body.appendChild(newItem)

// Check lengths again
console.log(liveList.length)    // 4 (automatically updated!)
console.log(staticList.length)  // 3 (still the old snapshot)
MethodReturnsLive?
getElementById()Element or nullN/A
getElementsByClassName()HTMLCollectionYes (live)
getElementsByTagName()HTMLCollectionYes (live)
querySelector()Element or nullN/A
querySelectorAll()NodeListNo (static)

Scoped Selection

You can call selection methods on any element, not just document:

javascript
const nav = document.querySelector('nav')

// Find links ONLY inside nav
const navLinks = nav.querySelectorAll('a')

// Find the active link inside nav
const activeLink = nav.querySelector('.active')

This is faster than searching the entire document and helps avoid selecting unintended elements.

Performance Comparison

<AccordionGroup> <Accordion title="Which selector method is fastest?"> In order of speed (fastest first):
1. **`getElementById()`** - Direct hashtable lookup, O(1)
2. **`getElementsByClassName()`** - Optimized internal lookup
3. **`getElementsByTagName()`** - Optimized internal lookup
4. **`querySelector()`** - Must parse CSS selector
5. **`querySelectorAll()`** - Must parse and find all matches

However, for most applications, **the difference is negligible**. Use `querySelector/querySelectorAll` for readability unless you're selecting thousands of elements in a loop.

```javascript
// Premature optimization - don't do this
const el1 = document.getElementById('myId')

// This is fine and more readable
const el2 = document.querySelector('#myId')
```
</Accordion> </AccordionGroup>

How to Traverse the DOM

Once you have an element, you can navigate to related elements without querying the entire document.

Traversing Downwards (To Children)

javascript
const ul = document.querySelector('ul')

// Get ALL child nodes (including text nodes!)
const allChildNodes = ul.childNodes      // NodeList

// Get only ELEMENT children (usually what you want)
const elementChildren = ul.children       // HTMLCollection

// Get specific children
const firstChild = ul.firstChild          // First node (might be text!)
const firstElement = ul.firstElementChild // First ELEMENT child
const lastChild = ul.lastChild            // Last node
const lastElement = ul.lastElementChild   // Last ELEMENT child
<Warning> **The Text Node Trap!** Look at this HTML:
html
<ul>
  <li>One</li>
  <li>Two</li>
</ul>

What is ul.firstChild? It's NOT the first <li>! It's a text node containing the newline and spaces after <ul>. Use firstElementChild to get the actual <li> element. </Warning>

Traversing Upwards (To Parents)

javascript
const li = document.querySelector('li')

// Direct parent
const parent = li.parentNode        // Usually same as parentElement
const parentEl = li.parentElement   // Guaranteed to be an Element (or null)

// Find ancestor matching selector (very useful!)
const form = li.closest('form')     // Finds nearest ancestor <form>
const card = li.closest('.card')    // Finds nearest ancestor with class "card"

// closest() includes the element itself
const self = li.closest('li')       // Returns li itself if it matches!

The closest() method is useful for event delegation (see Event Loop for how events are processed):

javascript
// Handle clicks on any button inside a card
document.addEventListener('click', (e) => {
  const card = e.target.closest('.card')
  if (card) {
    console.log('Clicked inside card:', card)
  }
})

Traversing Sideways (To Siblings)

javascript
const secondLi = document.querySelectorAll('li')[1]

// Previous/next nodes (might be text!)
const prevNode = secondLi.previousSibling
const nextNode = secondLi.nextSibling

// Previous/next ELEMENTS (usually what you want)
const prevElement = secondLi.previousElementSibling
const nextElement = secondLi.nextElementSibling

// Returns null at the boundaries
const firstLi = document.querySelector('li')
console.log(firstLi.previousElementSibling)  // null (no previous sibling)

Node vs Element Properties Cheat Sheet

Get...Node Property (includes text)Element Property (elements only)
ParentparentNodeparentElement
ChildrenchildNodeschildren
First childfirstChildfirstElementChild
Last childlastChildlastElementChild
Previous siblingpreviousSiblingpreviousElementSibling
Next siblingnextSiblingnextElementSibling
<Tip> **Rule of thumb:** Unless you specifically need text nodes, always use the Element variants (`children`, `firstElementChild`, `nextElementSibling`, etc.) </Tip>

Practical Example: Building a Breadcrumb Trail

javascript
// Get all ancestors of an element
function getAncestors(element) {
  const ancestors = []
  let current = element.parentElement
  
  while (current && current !== document.body) {
    ancestors.push(current)
    current = current.parentElement
  }
  
  return ancestors
}

const deepElement = document.querySelector('.deeply-nested')
console.log(getAncestors(deepElement))
// [<div.parent>, <section>, <main>, ...]

Creating and Manipulating Elements

The real power of the DOM is the ability to create, modify, and remove elements dynamically.

Creating Elements

Use createElement() to create new elements and createTextNode() to create text nodes:

javascript
// Create a new element
const div = document.createElement('div')
const span = document.createElement('span')
const img = document.createElement('img')

// Create a text node
const text = document.createTextNode('Hello, world!')

// Create a comment node
const comment = document.createComment('This is a comment')

// Elements are created "detached" - not yet in the DOM!
console.log(div.parentNode)  // null

Adding Elements to the DOM

There are many ways to add elements. Here's a comprehensive overview using methods like appendChild(), insertBefore(), append(), and prepend():

<Tabs> <Tab title="appendChild()"> Adds a node as the **last child** of a parent:
```javascript
const ul = document.querySelector('ul')
const li = document.createElement('li')
li.textContent = 'New item'

ul.appendChild(li)
// <ul>
//   <li>Existing</li>
//   <li>New item</li>  ← Added at the end
// </ul>
```
</Tab> <Tab title="insertBefore()"> Inserts a node **before** a reference node:
```javascript
const ul = document.querySelector('ul')
const existingLi = ul.querySelector('li')
const newLi = document.createElement('li')
newLi.textContent = 'First!'

ul.insertBefore(newLi, existingLi)
// <ul>
//   <li>First!</li>    ← Inserted before
//   <li>Existing</li>
// </ul>
```
</Tab> <Tab title="append() / prepend()"> Modern methods that accept multiple nodes AND strings:
```javascript
const div = document.querySelector('div')

// append() - adds to the END
div.append('Text', document.createElement('span'), 'More text')

// prepend() - adds to the START
div.prepend(document.createElement('strong'))
```
</Tab> <Tab title="before() / after()"> Insert as siblings (not children):
```javascript
const h1 = document.querySelector('h1')

// Insert BEFORE h1 (as previous sibling)
h1.before(document.createElement('nav'))

// Insert AFTER h1 (as next sibling)
h1.after(document.createElement('p'))
```
</Tab> </Tabs>

insertAdjacentHTML() - The Swiss Army Knife

For inserting HTML strings, insertAdjacentHTML() is powerful and fast:

javascript
const div = document.querySelector('div')

// Four positions to insert:
div.insertAdjacentHTML('beforebegin', '<p>Before div</p>')
div.insertAdjacentHTML('afterbegin', '<p>First child of div</p>')
div.insertAdjacentHTML('beforeend', '<p>Last child of div</p>')
div.insertAdjacentHTML('afterend', '<p>After div</p>')

Visual representation:

html
<!-- beforebegin -->
<div>
  <!-- afterbegin -->
  existing content
  <!-- beforeend -->
</div>
<!-- afterend -->

Removing Elements

<Tabs> <Tab title="remove()"> Modern and simple. Element removes itself:
```javascript
const element = document.querySelector('.to-remove')
element.remove()  // Gone!
```
</Tab> <Tab title="removeChild()"> Classic method. Remove via parent:
```javascript
const parent = document.querySelector('ul')
const child = parent.querySelector('li')
parent.removeChild(child)

// Or remove from any element
element.parentNode.removeChild(element)
```
</Tab> </Tabs>

Cloning Elements

Use cloneNode() to duplicate elements:

javascript
const original = document.querySelector('.card')

// Shallow clone (element only, no children)
const shallow = original.cloneNode(false)

// Deep clone (element AND all descendants)
const deep = original.cloneNode(true)

// Clones are detached - must add to DOM
document.body.appendChild(deep)
<Warning> **ID Collision!** If you clone an element with an ID, you'll have duplicate IDs in your document (invalid HTML). Remove or change the ID after cloning:
javascript
const clone = original.cloneNode(true)
clone.id = ''  // Remove ID
// or
clone.id = 'new-unique-id'
</Warning>

DocumentFragment - Batch Operations

When adding many elements, using a DocumentFragment is more efficient:

javascript
// Bad: Multiple DOM updates (potentially multiple reflows)
const ul = document.querySelector('ul')
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li')
  li.textContent = `Item ${i}`
  ul.appendChild(li)  // Modifies live DOM each iteration
}

// Good: Single DOM update
const ul = document.querySelector('ul')
const fragment = document.createDocumentFragment()

for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li')
  li.textContent = `Item ${i}`
  fragment.appendChild(li)  // No DOM update (fragment is detached)
}

ul.appendChild(fragment)  // Single DOM update!

A DocumentFragment is a lightweight container that:

  • Is not part of the DOM tree
  • Has no parent
  • When appended, only its children are inserted (the fragment itself disappears)
<Note> **Modern browser optimization:** Browsers may batch consecutive DOM modifications and perform a single reflow. However, using DocumentFragment is still the recommended pattern because it's explicit, works consistently across all browsers, and avoids any risk of forced synchronous layouts if you read layout properties between writes. </Note>

Modifying Content

Three properties let you read and write element content: innerHTML, textContent, and innerText.

innerHTML - Parse and Insert HTML

javascript
const div = document.querySelector('div')

// Read HTML content
console.log(div.innerHTML)  // "<p>Hello</p><span>World</span>"

// Write HTML content (parses the string!)
div.innerHTML = '<h1>New Title</h1><p>New paragraph</p>'

// Clear all content
div.innerHTML = ''
<Warning> **Security Alert: XSS Vulnerability!**

Never use innerHTML with user-provided content:

javascript
// DANGEROUS! User could inject: 
div.innerHTML = userInput  // NO!

// Safe alternatives:
div.textContent = userInput  // Escapes HTML
// or sanitize the input first
</Warning>

textContent - Plain Text Only

javascript
const div = document.querySelector('div')

// Read text (ignores HTML tags)
// <div><p>Hello</p><span>World</span></div>
console.log(div.textContent)  // "HelloWorld"

// Write text (HTML is escaped, not parsed)
div.textContent = '<script>alert("XSS")</script>'
// Displays literally: <script>alert("XSS")</script>
// Safe from XSS!

innerText - Rendered Text

javascript
const div = document.querySelector('div')

// innerText respects CSS visibility
// <div>Hello <span style="display:none">Hidden</span> World</div>

console.log(div.textContent)  // "Hello Hidden World"
console.log(div.innerText)    // "Hello  World" (Hidden is excluded!)

When to Use Each

PropertyUse Case
innerHTMLInserting trusted HTML (never user input!)
textContentSetting/getting plain text (safe, fast)
innerTextGetting text as user sees it (slower, respects CSS)
javascript
// Performance: textContent is faster than innerText
// because innerText must calculate styles

// Setting text content (both work, textContent is faster)
element.textContent = 'Hello'  // Preferred
element.innerText = 'Hello'    // Works but slower

How to Work with DOM Attributes

HTML elements have attributes. JavaScript lets you read, write, and remove them using getAttribute(), setAttribute(), hasAttribute(), and removeAttribute().

Standard Attribute Methods

javascript
const link = document.querySelector('a')

// Get attribute value
const href = link.getAttribute('href')
const target = link.getAttribute('target')

// Set attribute value
link.setAttribute('href', 'https://example.com')
link.setAttribute('target', '_blank')

// Check if attribute exists
if (link.hasAttribute('target')) {
  console.log('Link opens in new tab')
}

// Remove attribute
link.removeAttribute('target')

Properties vs Attributes: The Difference

This confuses many developers! Attributes are in the HTML. Properties are on the DOM object.

html
<input type="text" value="initial">
javascript
const input = document.querySelector('input')

// ATTRIBUTE: The original HTML value
console.log(input.getAttribute('value'))  // "initial"

// PROPERTY: The current state
console.log(input.value)  // "initial"

// User types "new text"...
console.log(input.getAttribute('value'))  // Still "initial"!
console.log(input.value)                  // "new text"

// Reset to attribute value
input.value = input.getAttribute('value')

Key differences:

AspectAttributeProperty
SourceHTML markupDOM object
Accessget/setAttribute()Direct property access
UpdatesManual onlyAutomatically with user interaction
TypeAlways stringCan be any type
javascript
// Attribute is always a string
checkbox.getAttribute('checked')  // "" or null

// Property is a boolean
checkbox.checked  // true or false

// Attribute (string)
input.getAttribute('maxlength')  // "10"

// Property (number)
input.maxLength  // 10

Data Attributes and the dataset API

Custom data attributes start with data- and are accessible via the dataset property:

html
<div id="user" 
     data-user-id="123" 
     data-role="admin"
     data-is-active="true">
  John Doe
</div>
javascript
const user = document.querySelector('#user')

// Read data attributes (camelCase!)
console.log(user.dataset.userId)    // "123"
console.log(user.dataset.role)      // "admin"
console.log(user.dataset.isActive)  // "true" (string, not boolean!)

// Write data attributes
user.dataset.lastLogin = '2024-01-15'
// Creates: data-last-login="2024-01-15"

// Delete data attributes
delete user.dataset.role

// Check if exists
if ('userId' in user.dataset) {
  console.log('Has user ID')
}
<Tip> **Naming Convention:** HTML uses `kebab-case` (`data-user-id`), JavaScript uses `camelCase` (`dataset.userId`). The conversion is automatic! </Tip>

Common Attribute Shortcuts

Many attributes have direct property shortcuts:

javascript
// These pairs are equivalent:
element.id                    // element.getAttribute('id')
element.className             // element.getAttribute('class')
element.href                  // element.getAttribute('href')
element.src                   // element.getAttribute('src')
element.title                 // element.getAttribute('title')

// For class manipulation, use classList (covered next)

How to Style DOM Elements with JavaScript

JavaScript can modify element styles in several ways using the style property and classList API.

The style Property (Inline Styles)

javascript
const box = document.querySelector('.box')

// Set individual styles (camelCase!)
box.style.backgroundColor = 'blue'
box.style.fontSize = '20px'
box.style.marginTop = '10px'

// Read styles (only reads INLINE styles!)
console.log(box.style.backgroundColor)  // "blue"
console.log(box.style.color)            // "" (not inline, from stylesheet)

// Set multiple styles at once
box.style.cssText = 'background: red; font-size: 16px; padding: 10px;'

// Remove an inline style
box.style.backgroundColor = ''  // Removes the style
<Warning> `element.style` only reads/writes **inline** styles! To get computed styles (from stylesheets), use `getComputedStyle()`. </Warning>

getComputedStyle() - Read Actual Styles

Use getComputedStyle() to read the final computed styles:

javascript
const box = document.querySelector('.box')

// Get all computed styles
const styles = getComputedStyle(box)

console.log(styles.backgroundColor)  // "rgb(0, 0, 255)"
console.log(styles.fontSize)         // "16px"
console.log(styles.display)          // "block"

// Get pseudo-element styles
const beforeStyles = getComputedStyle(box, '::before')
console.log(beforeStyles.content)    // '"Hello"'

classList - Manipulate CSS Classes

The classList API is the modern way to add/remove/toggle classes:

javascript
const button = document.querySelector('button')

// Add classes
button.classList.add('active')
button.classList.add('btn', 'btn-primary')  // Multiple at once

// Remove classes
button.classList.remove('active')
button.classList.remove('btn', 'btn-primary')  // Multiple at once

// Toggle (add if missing, remove if present)
button.classList.toggle('active')

// Toggle with condition
button.classList.toggle('active', isActive)  // Add if isActive is true

// Check if class exists
if (button.classList.contains('active')) {
  console.log('Button is active')
}

// Replace a class
button.classList.replace('btn-primary', 'btn-secondary')

// Iterate over classes
button.classList.forEach(cls => console.log(cls))

// Get number of classes
console.log(button.classList.length)  // 2

className vs classList

javascript
// className is a string (old way)
element.className = 'btn btn-primary'     // Replaces ALL classes
element.className += ' active'            // Appending is clunky

// classList is a DOMTokenList (modern way)
element.classList.add('active')           // Adds without affecting others
element.classList.remove('btn-primary')   // Removes specifically

How Browsers Render the DOM to Pixels

Understanding how browsers render pages helps you write performant code. This is where JavaScript Engines and the browser's rendering engine work together.

From HTML to Pixels

When you load a webpage, the browser goes through these steps:

<Steps> <Step title="1. Parse HTML → Build DOM"> Browser reads HTML bytes and constructs the Document Object Model tree. </Step> <Step title="2. Parse CSS → Build CSSOM"> CSS is parsed into the CSS Object Model with styling rules. </Step> <Step title="3. Combine → Render Tree"> DOM + CSSOM merge into the Render Tree (only visible elements). </Step> <Step title="4. Layout (Reflow)"> Calculate exact position and size of every element. </Step> <Step title="5. Paint"> Fill in pixels: colors, borders, shadows, text. </Step> <Step title="6. Composite"> Combine layers into the final image using the GPU. </Step> </Steps>
┌─────────────────────────────────────────────────────────────────────────────┐
│                     THE CRITICAL RENDERING PATH                              │
│                                                                              │
│  1. PARSE HTML          2. PARSE CSS           3. BUILD RENDER TREE         │
│  ┌──────────────┐      ┌──────────────┐       ┌──────────────────────┐      │
│  │  HTML bytes  │      │  CSS bytes   │       │  DOM    +   CSSOM    │      │
│  │      ↓       │      │      ↓       │       │    ↘     ↙           │      │
│  │  Characters  │      │  Characters  │       │   RENDER TREE        │      │
│  │      ↓       │      │      ↓       │       │  (visible elements   │      │
│  │   Tokens     │      │   Tokens     │       │   + their styles)    │      │
│  │      ↓       │      │      ↓       │       └──────────────────────┘      │
│  │    Nodes     │      │    Rules     │                  │                  │
│  │      ↓       │      │      ↓       │                  ▼                  │
│  │    DOM       │      │   CSSOM      │       4. LAYOUT (Reflow)            │
│  └──────────────┘      └──────────────┘       ┌──────────────────────┐      │
│                                               │ Calculate exact      │      │
│                                               │ position & size of   │      │
│                                               │ every element        │      │
│                                               └──────────┬───────────┘      │
│                                                          │                  │
│                                                          ▼                  │
│                                               5. PAINT                      │
│                                               ┌──────────────────────┐      │
│                                               │ Fill in pixels:      │      │
│                                               │ colors, borders,     │      │
│                                               │ shadows, text        │      │
│                                               └──────────┬───────────┘      │
│                                                          │                  │
│                                                          ▼                  │
│                                               6. COMPOSITE                  │
│                                               ┌──────────────────────┐      │
│                                               │ Combine layers into  │      │
│                                               │ final image (GPU)    │      │
│                                               └──────────────────────┘      │
│                                                          │                  │
│                                                          ▼                  │
│                                               ┌──────────────────────┐      │
│                                               │      PIXELS!         │      │
│                                               └──────────────────────┘      │
└─────────────────────────────────────────────────────────────────────────────┘

What's NOT in the Render Tree

The Render Tree only contains visible elements:

html
<!-- NOT in Render Tree -->
<head>...</head>                    <!-- head is never rendered -->
<script>...</script>                <!-- script tags aren't visible -->
<link rel="stylesheet">             <!-- link tags aren't visible -->
<meta>                              <!-- meta tags aren't visible -->
<div style="display: none">Hi</div> <!-- display:none excluded -->

<!-- IN the Render Tree (even if not seen) -->
<div style="visibility: hidden">Hi</div>  <!-- Takes up space -->
<div style="opacity: 0">Hi</div>          <!-- Takes up space -->

Layout (Reflow) - The Expensive Step

Layout calculates the geometry of every element: position, size, margins, etc.

Reflow is triggered when:

  • Adding/removing elements
  • Changing element dimensions (width, height, padding, margin)
  • Changing font size
  • Resizing the window
  • Reading certain properties (more on this below!)

Paint - Drawing Pixels

After layout, the browser paints the pixels: text, colors, images, borders, shadows.

Repaint (without reflow) happens when:

  • Changing colors
  • Changing background-image
  • Changing visibility
  • Changing box-shadow (sometimes)

Composite - Layering

Modern browsers separate content into layers and use the GPU to composite them. This is why some animations are smooth:

css
/* These properties can animate without reflow/repaint */
transform: translateX(100px);  /* GPU accelerated! */
opacity: 0.5;                   /* GPU accelerated! */

/* These properties cause reflow */
left: 100px;    /* Avoid for animations! */
width: 200px;   /* Avoid for animations! */

How to Optimize DOM Performance

DOM operations can be slow. Here's how to keep your pages fast.

Cache DOM References

javascript
// Bad: Queries the DOM every iteration
for (let i = 0; i < 1000; i++) {
  document.querySelector('.result').textContent += i
}

// Good: Query once, reuse
const result = document.querySelector('.result')
for (let i = 0; i < 1000; i++) {
  result.textContent += i
}

// Even better: Build string, set once
const result = document.querySelector('.result')
let text = ''
for (let i = 0; i < 1000; i++) {
  text += i
}
result.textContent = text

Batch DOM Updates

javascript
// Avoid: Multiple style changes (may trigger multiple reflows)
element.style.width = '100px'
element.style.height = '200px'
element.style.margin = '10px'

// Better: Single style assignment with cssText
element.style.cssText = 'width: 100px; height: 200px; margin: 10px;'

// Best: Use a CSS class (cleanest and most maintainable)
element.classList.add('my-styles')

// Good: DocumentFragment for multiple elements
const fragment = document.createDocumentFragment()
items.forEach(item => {
  const li = document.createElement('li')
  li.textContent = item
  fragment.appendChild(li)
})
ul.appendChild(fragment)  // Single DOM update
<Tip> **Why batch?** While modern browsers often optimize consecutive style changes into a single reflow, this optimization breaks if you read a layout property (like `offsetWidth`) between writes. Batching explicitly avoids this risk and makes your intent clear. </Tip>

Avoid Layout Thrashing

Layout thrashing occurs when you alternate between reading and writing DOM properties:

javascript
// TERRIBLE: Forces layout on EVERY iteration
boxes.forEach(box => {
  const width = box.offsetWidth      // Read (forces layout)
  box.style.width = (width + 10) + 'px'  // Write (invalidates layout)
})

// GOOD: Batch reads, then batch writes
const widths = boxes.map(box => box.offsetWidth)  // Read all
boxes.forEach((box, i) => {
  box.style.width = (widths[i] + 10) + 'px'       // Write all
})

Properties that trigger layout when read:

PropertyWhat It Returns
offsetWidth / offsetHeightElement's layout width/height including borders
offsetTop / offsetLeftPosition relative to offset parent
clientWidth / clientHeightInner dimensions (padding but no border)
scrollWidth / scrollHeightFull scrollable dimensions
scrollTop / scrollLeftCurrent scroll position
getBoundingClientRect()Position and size relative to viewport
getComputedStyle()All computed CSS values
javascript
// Any of these reads forces a layout calculation
const width = element.offsetWidth      // Layout triggered!
const rect = element.getBoundingClientRect()  // Layout triggered!
const styles = getComputedStyle(element)      // Layout triggered!

Use requestAnimationFrame for Visual Changes

Use requestAnimationFrame() to batch visual changes with the browser's render cycle:

javascript
// Bad: DOM changes at unpredictable times
window.addEventListener('scroll', () => {
  element.style.transform = `translateY(${window.scrollY}px)`
})

// Good: Batch visual changes with next frame
let ticking = false
window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      element.style.transform = `translateY(${window.scrollY}px)`
      ticking = false
    })
    ticking = true
  }
})

The #1 DOM Mistake: Using innerHTML with User Input

The most dangerous DOM mistake is using innerHTML with untrusted content. This opens your application to Cross-Site Scripting (XSS) attacks.

┌─────────────────────────────────────────────────────────────────────────┐
│                    innerHTML: THE SECURITY TRAP                          │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  ❌ DANGEROUS                              ✓ SAFE                        │
│  ─────────────                             ──────                        │
│                                                                          │
│  User Input:                               User Input:                   │
│  ""       ""     │
│         │                                        │                       │
│         ▼                                        ▼                       │
│  element.innerHTML = userInput             element.textContent = input   │
│         │                                        │                       │
│         ▼                                        ▼                       │
│  ┌─────────────────┐                      ┌─────────────────┐            │
│  │ BROWSER PARSES  │                      │ DISPLAYED AS    │            │
│  │ AS REAL HTML!   │                      │ PLAIN TEXT      │            │
│  │                 │                      │                 │            │
│  │ 🚨 Script runs! │                      │ "
div.innerHTML = `Welcome, ${username}!`
// The malicious script EXECUTES!

// ✓ SAFE - textContent escapes HTML
const username = getUserInput()
div.textContent = `Welcome, ${username}!`
// Displays: Welcome, !
// The HTML is shown as text, not executed

// ✓ SAFE - Create elements programmatically
const username = getUserInput()
const welcomeText = document.createTextNode(`Welcome, ${username}!`)
div.appendChild(welcomeText)
<Warning> **The Trap:** `innerHTML` looks convenient, but it parses strings as real HTML. If that string contains user input, attackers can inject `<script>` tags, malicious event handlers, or other dangerous code. **Always use `textContent` for user-provided content.** </Warning>

Other Common Mistakes

<AccordionGroup> <Accordion title="Forgetting that querySelector returns null"> ```javascript // ❌ WRONG - Crashes if element doesn't exist document.querySelector('.maybe-missing').classList.add('active') // TypeError: Cannot read property 'classList' of null
// ✓ CORRECT - Check first or use optional chaining
const element = document.querySelector('.maybe-missing')
if (element) {
  element.classList.add('active')
}

// Or use optional chaining (modern)
document.querySelector('.maybe-missing')?.classList.add('active')
```
</Accordion> <Accordion title="Using childNodes instead of children"> ```javascript // ❌ CONFUSING - Includes whitespace text nodes! const ul = document.querySelector('ul') console.log(ul.childNodes.length) // 7 (includes text nodes!)
// ✓ CLEAR - Only element children
console.log(ul.children.length)  // 3 (just the <li> elements)
```
</Accordion> <Accordion title="Layout thrashing in loops"> ```javascript // ❌ SLOW - Forces layout on every iteration boxes.forEach(box => { const width = box.offsetWidth // READ - forces layout box.style.width = width + 10 + 'px' // WRITE - invalidates layout })
// ✓ FAST - Batch reads, then batch writes
const widths = boxes.map(box => box.offsetWidth)  // All reads
boxes.forEach((box, i) => {
  box.style.width = widths[i] + 10 + 'px'          // All writes
})
```
</Accordion> </AccordionGroup>

Event Propagation: Bubbling and Capturing

When an event occurs on a DOM element, it doesn't just trigger on that element. It travels through the DOM tree in a process called event propagation. Understanding this helps with event handling.

The Three Phases

Every DOM event goes through three phases:

1. CAPTURING PHASE    ↓  (from window → target's parent)
2. TARGET PHASE       ●  (at the target element)
3. BUBBLING PHASE     ↑  (from target's parent → window)
javascript
// Most events bubble UP by default
document.querySelector('.child').addEventListener('click', (e) => {
  console.log('Child clicked')
})

document.querySelector('.parent').addEventListener('click', (e) => {
  console.log('Parent also receives the click!')  // This fires too!
})

Capturing vs Bubbling

By default, event listeners fire during the bubbling phase (bottom-up). You can listen during the capturing phase (top-down) with the third parameter:

javascript
// Bubbling (default) — fires on the way UP
element.addEventListener('click', handler)
element.addEventListener('click', handler, false)

// Capturing — fires on the way DOWN
element.addEventListener('click', handler, true)
element.addEventListener('click', handler, { capture: true })
javascript
// Practical example: see the order
document.querySelector('.parent').addEventListener('click', () => {
  console.log('1. Parent - capturing')
}, true)

document.querySelector('.child').addEventListener('click', () => {
  console.log('2. Child - target')
})

document.querySelector('.parent').addEventListener('click', () => {
  console.log('3. Parent - bubbling')
})

// Click on child outputs: 1, 2, 3

Stopping Propagation

You can stop an event from traveling further:

javascript
element.addEventListener('click', (e) => {
  e.stopPropagation()        // Stop bubbling/capturing
  // Parent handlers won't fire
})

element.addEventListener('click', (e) => {
  e.stopImmediatePropagation()  // Stop ALL handlers, even on same element
})
<Warning> **Use `stopPropagation()` sparingly!** It breaks event delegation and can make debugging difficult. Usually there's a better solution. </Warning>

Preventing Default Behavior

Don't confuse propagation with default behavior:

javascript
// Prevent the browser's default action (e.g., following a link)
link.addEventListener('click', (e) => {
  e.preventDefault()   // Don't navigate
  // Event still bubbles unless you also call stopPropagation()
})

// Common use cases:
// - Prevent form submission: form.addEventListener('submit', e => e.preventDefault())
// - Prevent link navigation: link.addEventListener('click', e => e.preventDefault())
// - Prevent context menu: element.addEventListener('contextmenu', e => e.preventDefault())

The event.target vs event.currentTarget

This distinction matters for event delegation:

javascript
document.querySelector('.parent').addEventListener('click', (e) => {
  console.log(e.target)        // The element that was actually clicked
  console.log(e.currentTarget) // The element with the listener (.parent)
  console.log(this)            // Same as currentTarget (in regular functions)
})
javascript
// If you click on a <span> inside .parent:
// e.target = <span>           (what you clicked)
// e.currentTarget = .parent   (what has the listener)

Events That Don't Bubble

Most events bubble, but some don't:

EventBubbles?Notes
click, mousedown, keydownYesMost user events bubble
focus, blurNoUse focusin/focusout for bubbling versions
mouseenter, mouseleaveNoUse mouseover/mouseout for bubbling versions
load, unload, scrollNoWindow/document events
javascript
// focus doesn't bubble, but focusin does
form.addEventListener('focusin', (e) => {
  console.log('Something in the form was focused:', e.target)
})

Common DOM Patterns

Event Delegation

Instead of adding listeners to many elements, add one to a parent. This pattern relies on event bubbling. When you click a child element, the event bubbles up to the parent where your listener catches it:

javascript
// Bad: Many listeners
document.querySelectorAll('.btn').forEach(btn => {
  btn.addEventListener('click', handleClick)
})

// Good: One listener with delegation
document.querySelector('.button-container').addEventListener('click', (e) => {
  const btn = e.target.closest('.btn')
  if (btn) {
    handleClick(e)
  }
})

Benefits:

  • Works for dynamically added elements
  • Less memory usage
  • Easier cleanup (uses closures to maintain handler references)

Checking if Element Exists

javascript
// Using querySelector (returns null if not found)
const element = document.querySelector('.maybe-exists')
if (element) {
  element.textContent = 'Found!'
}

// Optional chaining (modern)
document.querySelector('.maybe-exists')?.classList.add('active')

// With getElementById
const el = document.getElementById('myId')
if (el !== null) {
  // Element exists
}

Waiting for DOM Ready

Listen for the DOMContentLoaded event to know when the DOM is ready:

javascript
// Modern: DOMContentLoaded (DOM ready, images may still be loading)
document.addEventListener('DOMContentLoaded', () => {
  console.log('DOM is ready!')
  // Safe to query elements
})

// Full page load (including images, stylesheets)
window.addEventListener('load', () => {
  console.log('Everything loaded!')
})

// If script is at end of body, DOM is already ready
// <script src="app.js"></script> <!-- Just before </body> -->

// Modern: defer attribute (script loads in parallel, runs after DOM ready)
// <script src="app.js" defer></script>
<Tip> **Best practice:** Put your `<script>` tags just before `</body>` or use the `defer` attribute. Then you don't need to wait for DOMContentLoaded. </Tip>

Common Misconceptions

<AccordionGroup> <Accordion title="Misconception 1: 'The DOM is the same as my HTML source code'"> **Wrong!** The DOM is NOT your HTML file. The browser:
1. **Fixes errors** — Missing `<head>`, `<body>`, unclosed tags are auto-corrected
2. **Normalizes structure** — Text outside elements gets wrapped properly
3. **Reflects JavaScript changes** — DOM updates don't change your HTML file

```html
<!-- Your HTML file -->
<html>Hello World

<!-- What the DOM looks like -->
<html>
  <head></head>
  <body>Hello World</body>
</html>
```

View Source shows your file. DevTools Elements shows the DOM.
</Accordion> <Accordion title="Misconception 2: 'querySelector is slow, use getElementById'"> **Mostly wrong!** Yes, `getElementById` is technically faster (O(1) hashtable lookup), but:
- The difference is **microseconds** — imperceptible to users
- `querySelector` is more **flexible** and **readable**
- You'd need to call it **thousands of times in a loop** to notice

```javascript
// Both are fine for normal use
document.getElementById('myId')
document.querySelector('#myId')

// Only optimize if you're selecting in a tight loop
// with performance issues (rare!)
```

**Rule:** Write readable code first. Optimize only when you have a measured problem.
</Accordion> <Accordion title="Misconception 3: 'display: none removes the element from the DOM'"> **Wrong!** `display: none` hides the element visually, but it's still in the DOM:
```javascript
element.style.display = 'none'

// Element is STILL in the DOM!
console.log(document.getElementById('hidden'))  // Element exists
console.log(element.parentNode)                 // Still has parent

// To actually remove from DOM:
element.remove()
// or
element.parentNode.removeChild(element)
```

- `display: none` → Hidden but in DOM, not in Render Tree
- `visibility: hidden` → Hidden but takes up space, in Render Tree
- `remove()` → Actually removed from DOM
</Accordion> <Accordion title="Misconception 4: 'Live collections automatically update my code'"> **Misleading!** Live collections (`getElementsByClassName`, `getElementsByTagName`) update automatically, but this can cause bugs:
```javascript
const items = document.getElementsByClassName('item')

// DANGER: Removing items changes the collection while looping!
for (let i = 0; i < items.length; i++) {
  items[i].remove()  // Collection shrinks, indices shift!
}
// Some items are skipped!

// SAFE: Use static NodeList or convert to array
const items = document.querySelectorAll('.item')  // Static
items.forEach(item => item.remove())  // Works correctly
```

**Tip:** Prefer `querySelectorAll` (static) unless you specifically need live updates.
</Accordion> </AccordionGroup>

Classic Interview Questions

Question 1: What's the difference between document.querySelector and document.getElementById?

<Accordion title="Answer"> | Feature | `getElementById` | `querySelector` | |---------|-----------------|-----------------| | Selector type | ID only | Any CSS selector | | Returns | Element or `null` | Element or `null` | | Speed | Faster (hashtable) | Slightly slower (parses CSS) | | Flexibility | Low | High |
javascript
// getElementById — only IDs
document.getElementById('myId')

// querySelector — any CSS selector
document.querySelector('#myId')           // Same as above
document.querySelector('.card:first-child')  // Not possible with getElementById
document.querySelector('[data-id="123"]')    // Attribute selector

Best answer: "getElementById is marginally faster but querySelector is more flexible. In practice, the performance difference is negligible for most applications. I prefer querySelector for consistency and flexibility." </Accordion>

Question 2: Explain event delegation and why it's useful

<Accordion title="Answer"> **Event delegation** is attaching a single event listener to a parent element instead of multiple listeners to child elements. It works because events "bubble up" the DOM tree.
javascript
// ❌ Without delegation — 100 listeners for 100 items
document.querySelectorAll('.item').forEach(item => {
  item.addEventListener('click', handleClick)
})

// ✓ With delegation — 1 listener handles all items
document.querySelector('.container').addEventListener('click', (e) => {
  const item = e.target.closest('.item')
  if (item) handleClick(e)
})

Benefits:

  1. Memory efficient — One listener vs. many
  2. Works for dynamic elements — New items automatically handled
  3. Easier cleanup — Remove one listener to clean up

Best answer: Include a code example and mention closest() for finding the target element. </Accordion>

Question 3: What causes layout thrashing and how do you avoid it?

<Accordion title="Answer"> **Layout thrashing** occurs when you repeatedly alternate between reading and writing DOM layout properties, forcing the browser to recalculate layout multiple times.
javascript
// ❌ Thrashing — forces layout on EVERY iteration
boxes.forEach(box => {
  const width = box.offsetWidth     // READ → triggers layout
  box.style.width = width + 10 + 'px' // WRITE → invalidates layout
})

// ✓ Batched — one layout calculation
const widths = boxes.map(box => box.offsetWidth)  // All reads
boxes.forEach((box, i) => {
  box.style.width = widths[i] + 10 + 'px'          // All writes
})

Properties that trigger layout: offsetWidth/Height, clientWidth/Height, getBoundingClientRect(), getComputedStyle()

Best answer: Explain the read-write-read-write pattern and show the batched solution. </Accordion>

Question 4: What's the difference between innerHTML, textContent, and innerText?

<Accordion title="Answer"> | Property | Parses HTML? | Includes hidden text? | Performance | Security | |----------|-------------|----------------------|-------------|----------| | `innerHTML` | Yes | Yes | Slower | XSS risk | | `textContent` | No | Yes | Fast | Safe | | `innerText` | No | No (respects CSS) | Slowest | Safe |
javascript
// <div id="el"><span style="display:none">Hidden</span> Visible</div>

el.innerHTML     // "<span style="display:none">Hidden</span> Visible"
el.textContent   // "Hidden Visible"
el.innerText     // " Visible" (hidden text excluded)

Security warning: Never use innerHTML with user input. It can execute malicious scripts (XSS attacks). Use textContent instead.

Best answer: Mention the XSS security risk with innerHTML. This shows you understand real-world implications. </Accordion>

Question 5: How do you efficiently add 1000 elements to the DOM?

<Accordion title="Answer"> Use a **DocumentFragment** to batch insertions:
javascript
// ❌ Slow — 1000 DOM updates, 1000 potential reflows
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li')
  li.textContent = `Item ${i}`
  ul.appendChild(li)  // Triggers update each time
}

// ✓ Fast — 1 DOM update
const fragment = document.createDocumentFragment()
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li')
  li.textContent = `Item ${i}`
  fragment.appendChild(li)  // No DOM update (fragment is detached)
}
ul.appendChild(fragment)  // Single update

Alternative: Build an HTML string and use innerHTML once (but only with trusted content, never user input).

Best answer: Show the fragment approach and explain WHY it's faster (detached container, single reflow). </Accordion>

Question 6: What's the difference between attributes and properties?

<Accordion title="Answer"> **Attributes** are defined in HTML. **Properties** are the live state on DOM objects.
html
<input type="text" value="initial">
javascript
const input = document.querySelector('input')

// Attribute — original HTML value
input.getAttribute('value')  // "initial" (never changes)

// Property — current live value
input.value  // "initial" initially, then whatever user types

// User types "hello"...
input.getAttribute('value')  // Still "initial"
input.value                  // "hello"
AspectAttributeProperty
SourceHTML markupDOM object
TypeAlways stringCan be any type
UpdatesManual onlyAutomatically with interaction

Best answer: Use the <input value=""> example. It's the clearest demonstration of the difference. </Accordion>


Key Takeaways

<Info> **The key things to remember:**
  1. The DOM is a tree — Elements are nodes with parent, child, and sibling relationships

  2. DOM ≠ HTML source — The browser fixes errors and JavaScript modifies it

  3. Use querySelector — More flexible than getElementById, accepts any CSS selector

  4. Element vs Node properties — Use children, firstElementChild, etc. to skip text nodes

  5. closest() is your friend — Perfect for event delegation and finding ancestor elements

  6. innerHTML is dangerous — Never use with user input; use textContent instead

  7. Attributes vs Properties — Attributes are HTML source, properties are live DOM state

  8. classList over className — Use add/remove/toggle for cleaner class manipulation

  9. Batch DOM operations — Use DocumentFragment or build strings to minimize reflows

  10. Avoid layout thrashing — Don't alternate reading and writing layout properties

    </Info>

Test Your Knowledge

<AccordionGroup> <Accordion title="Question 1: What's the difference between childNodes and children?"> **Answer:**
- `childNodes` returns ALL child nodes, including **text nodes** (whitespace!) and **comment nodes**
- `children` returns only **element nodes**

```javascript
// <ul>
//   <li>One</li>
//   <li>Two</li>
// </ul>

ul.childNodes.length  // 5 (text, li, text, li, text)
ul.children.length    // 2 (li, li)
```

**Rule:** Use `children` unless you specifically need text/comment nodes.
</Accordion> <Accordion title="Question 2: Why is innerHTML dangerous with user input?"> **Answer:** `innerHTML` parses strings as HTML, enabling **Cross-Site Scripting (XSS)** attacks:
```javascript
// User input: 
div.innerHTML = userInput  // Executes malicious code!

// Safe: textContent escapes HTML
div.textContent = userInput  // Displays as plain text
```

Always sanitize HTML or use `textContent` for user-provided content.
</Accordion> <Accordion title="Question 3: What's the difference between getAttribute('value') and .value on an input?"> **Answer:**
- `getAttribute('value')` returns the **original HTML attribute** (initial value)
- `.value` property returns the **current value** (what user typed)

```javascript
// <input value="initial">
// User types "hello"

input.getAttribute('value')  // "initial"
input.value                  // "hello"
```

Attributes are the HTML source. Properties are the live DOM state.
</Accordion> <Accordion title="Question 4: What does closest() do and why is it useful?"> **Answer:** `closest()` finds the nearest **ancestor** (including the element itself) that matches a selector:
```javascript
// <div class="card">
//   <button class="btn">Click</button>
// </div>

btn.closest('.card')  // Returns the parent div
btn.closest('button') // Returns btn itself (it matches!)
btn.closest('.modal') // null (no matching ancestor)
```

**Super useful for event delegation:**

```javascript
document.addEventListener('click', (e) => {
  const card = e.target.closest('.card')
  if (card) {
    // Handle click inside any card
  }
})
```
</Accordion> <Accordion title="Question 5: What causes layout thrashing and how do you avoid it?"> **Answer:** Layout thrashing happens when you **alternate reading and writing** layout-triggering properties:
```javascript
// BAD: Read-write-read-write pattern
boxes.forEach(box => {
  const width = box.offsetWidth     // READ → forces layout
  box.style.width = width + 10 + 'px' // WRITE → invalidates layout
})
// Each iteration forces a new layout calculation!

// GOOD: Batch reads, then batch writes
const widths = boxes.map(b => b.offsetWidth)  // All reads
boxes.forEach((box, i) => {
  box.style.width = widths[i] + 10 + 'px'     // All writes
})
// Only one layout calculation!
```
</Accordion> <Accordion title="Question 6: What's in the Render Tree vs the DOM?"> **Answer:** The DOM contains **all nodes** from the HTML (plus JS modifications). The Render Tree contains only **visible elements** with their computed styles.
**In DOM but NOT in Render Tree:**
- `<head>` and its contents
- `<script>`, `<link>`, `<meta>` tags
- Elements with `display: none`

**In Render Tree:**
- Visible elements
- Elements with `visibility: hidden` (still take space)
- Elements with `opacity: 0` (still take space)

Pseudo-elements (`::before`, `::after`) are in the Render Tree but NOT in the DOM.
</Accordion> <Accordion title="Question 7: getElementsByClassName vs querySelectorAll - what's different?"> **Answer:**
| Aspect | `getElementsByClassName` | `querySelectorAll` |
|--------|--------------------------|-------------------|
| Returns | HTMLCollection | NodeList |
| **Live** | **Yes** (updates automatically) | **No** (static snapshot) |
| Selector | Class name only | Any CSS selector |
| Speed | Slightly faster | Slightly slower |

```javascript
const live = document.getElementsByClassName('item')
const staticList = document.querySelectorAll('.item')

// Add new element with class="item"
document.body.appendChild(newItem)

live.length       // Increased (live collection)
staticList.length // Same (static snapshot)
```
</Accordion> <Accordion title="Question 8: How do you safely add many elements to the DOM?"> **Answer:** Use a **DocumentFragment** to batch insertions:
```javascript
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 detached)
}

ul.appendChild(fragment)  // Single reflow!
```

A DocumentFragment is a virtual container. When appended, only its children are inserted. The fragment disappears.

Alternative: Build HTML string and use `innerHTML` once (but sanitize if user input!).
</Accordion> </AccordionGroup>

Frequently Asked Questions

<AccordionGroup> <Accordion title="What is the DOM in JavaScript?"> The DOM (Document Object Model) is a programming interface that represents an HTML document as a tree of objects. As defined by the WHATWG DOM Living Standard, it provides a structured representation that JavaScript can read and modify. Every element, attribute, and text node becomes an object in this tree, allowing dynamic page manipulation. </Accordion> <Accordion title="What is the difference between the DOM and HTML?"> HTML is the static markup you write in a file. The DOM is the live, in-memory representation the browser creates after parsing that HTML. The browser corrects errors (adding missing tags, fixing nesting), executes JavaScript that modifies it, and keeps it in sync with what you see on screen. The DOM can differ significantly from your original HTML source. </Accordion> <Accordion title="What is the difference between getElementById and querySelector?"> `getElementById` finds a single element by its `id` attribute and is the fastest DOM lookup method. `querySelector` accepts any CSS selector and is more flexible, but slightly slower. According to MDN, `getElementById` is only available on the `document` object, while `querySelector` can be called on any element to search within its subtree. </Accordion> <Accordion title="What causes layout thrashing in the DOM?"> Layout thrashing occurs when you repeatedly read layout properties (like `offsetHeight`) and then write to the DOM in the same synchronous block. Each read forces the browser to recalculate layout before returning a value, and each write invalidates that layout. According to Google's web performance research, layout thrashing is one of the most common causes of janky scrolling and slow interactions. </Accordion> <Accordion title="How does event delegation work in JavaScript?"> Event delegation attaches a single event listener to a parent element instead of adding listeners to every child. It works because DOM events bubble up from the target through ancestor elements. You check `event.target` to identify which child was clicked. This pattern is more memory-efficient and works automatically for dynamically added elements. </Accordion> </AccordionGroup>
<CardGroup cols={2}> <Card title="Event Loop" icon="arrows-spin" href="/concepts/event-loop"> How JavaScript handles async operations and DOM events </Card> <Card title="JavaScript Engines" icon="gear" href="/concepts/javascript-engines"> How V8 and other engines parse and execute your DOM code </Card> <Card title="Scope and Closures" icon="layer-group" href="/concepts/scope-and-closures"> Understanding variable scope in event handlers and callbacks </Card> <Card title="Design Patterns" icon="puzzle-piece" href="/concepts/design-patterns"> Patterns like Observer for reactive DOM updates </Card> </CardGroup>

Reference

<CardGroup cols={2}> <Card title="Document Object Model (DOM) — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model"> The comprehensive MDN reference for all DOM interfaces, methods, and properties. </Card> <Card title="Document Interface — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Document"> The Document interface representing the web page loaded in the browser. </Card> <Card title="Element Interface — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Element"> The base class for all element objects in a Document. </Card> <Card title="Node Interface — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/Node"> The abstract base class for DOM nodes including elements, text, and comments. </Card> <Card title="NodeList Interface — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/NodeList"> Collections of nodes returned by querySelectorAll and other methods. </Card> <Card title="HTMLCollection Interface — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection"> Live collections of elements returned by getElementsByClassName and similar. </Card> </CardGroup>

Articles

<CardGroup cols={2}> <Card title="Eloquent JavaScript: The Document Object Model" icon="book" href="https://eloquentjavascript.net/14_dom.html"> A free book chapter with runnable code examples you can edit right in the browser. Includes exercises at the end to test your understanding. </Card> <Card title="How To Understand and Modify the DOM in JavaScript" icon="newspaper" href="https://www.digitalocean.com/community/tutorials/introduction-to-the-dom"> Tania Rascia walks through each concept with side-by-side HTML and JavaScript examples. Great for visual learners who want to see code and results together. </Card> <Card title="What's the Document Object Model, and why you should know how to use it" icon="newspaper" href="https://medium.freecodecamp.org/whats-the-document-object-model-and-why-you-should-know-how-to-use-it-1a2d0bc5429d"> Builds a simple project while explaining DOM concepts. Good if you learn better by building something rather than reading theory. </Card> <Card title="What is the DOM?" icon="newspaper" href="https://css-tricks.com/dom/"> Short read that clears up the "DOM vs HTML source" confusion with visual examples. Explains why DevTools shows something different from View Source. </Card> <Card title="Traversing the DOM with JavaScript" icon="newspaper" href="https://zellwk.com/blog/dom-traversals/"> Zell explains the difference between Node and Element traversal methods with clear diagrams. Includes the "whitespace text node" gotcha that trips up beginners. </Card> <Card title="DOM Tree" icon="newspaper" href="https://javascript.info/dom-nodes"> Interactive examples you can edit and run in the browser. Part of a larger DOM tutorial series if you want to keep going deeper. </Card> <Card title="How to traverse the DOM in JavaScript" icon="newspaper" href="https://medium.com/javascript-in-plain-english/how-to-traverse-the-dom-in-javascript-d6555c335b4e"> Covers every traversal method with console output screenshots. Useful reference when you forget which property to use for siblings vs children. </Card> <Card title="Render Tree Construction" icon="newspaper" href="https://web.dev/articles/critical-rendering-path/render-tree-construction"> Google's official explanation of the Critical Rendering Path. Essential reading if you want to understand why some DOM operations are slow. </Card> <Card title="What, exactly, is the DOM?" icon="newspaper" href="https://bitsofco.de/what-exactly-is-the-dom/"> Compares DOM vs HTML source vs Render Tree side by side with diagrams. Clears up the confusion about what DevTools actually shows you. </Card> <Card title="JavaScript DOM Tutorial" icon="newspaper" href="https://www.javascripttutorial.net/javascript-dom/"> A multi-part tutorial organized by topic, so you can jump to exactly what you need. Each page is self-contained with try-it-yourself examples. </Card> <Card title="Event Propagation — MDN" icon="newspaper" href="https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events"> MDN's guide to event handling including bubbling, capturing, and delegation patterns. </Card> <Card title="Bubbling and Capturing" icon="newspaper" href="https://javascript.info/bubbling-and-capturing"> Animated diagrams showing events traveling up and down the DOM tree. Makes the three-phase model (capture, target, bubble) easy to visualize. </Card> </CardGroup>

Videos

<CardGroup cols={2}> <Card title="JavaScript DOM Manipulation – Full Course for Beginners" icon="graduation-cap" href="https://www.youtube.com/watch?v=5fb2aPlgoys"> A 2-hour freeCodeCamp course that builds multiple projects while teaching DOM concepts. Good if you want structured learning from zero to comfortable. </Card> <Card title="JavaScript DOM Tutorial" icon="video" href="https://www.youtube.com/watch?v=FIORjGvT0kk"> A playlist of short, focused videos (5-10 min each). Pick the topic you need instead of watching everything in order. </Card> <Card title="JavaScript DOM Crash Course" icon="video" href="https://www.youtube.com/watch?v=0ik6X4DJKCc"> Brad Traversy's 4-part series (this is part 1). Builds a task list project by the end, so you see DOM skills applied to something real. </Card> <Card title="JavaScript DOM Manipulation Methods" icon="video" href="https://www.youtube.com/watch?v=y17RuWkWdn8"> Web Dev Simplified explains createElement, appendChild, and other manipulation methods. </Card> <Card title="JavaScript DOM Traversal Methods" icon="video" href="https://www.youtube.com/watch?v=v7rSSy8CaYE"> Web Dev Simplified covers parent, child, and sibling traversal methods. </Card> <Card title="Event Propagation - JavaScript Event Bubbling and Propagation" icon="video" href="https://www.youtube.com/watch?v=JYc7gr9Ehl0"> Steve Griffith explains event bubbling, capturing, and how to control event flow. </Card> </CardGroup>