The Document Object Model (DOM)
The DOM is the bridge between HTML and JavaScript — it turns your static markup into a living structure your code can read, change, and bring to life.
What is the DOM?
The Document Object Model (DOM) is a programming interface that browsers create when they load an HTML page. It represents your entire HTML document as a structured tree of objects — each tag, attribute, and piece of text becomes a node in this tree.
Think of it this way: HTML is the blueprint, but the DOM is the actual building the browser constructs from that blueprint. Once the building exists, JavaScript can walk into any room, rearrange the furniture, add new rooms, or tear down walls — all in real time, without reloading the page.
The DOM is not part of the JavaScript language itself. It's a Web API provided by the browser. JavaScript simply uses this API to interact with your page. In theory, any programming language could use the DOM — JavaScript just happens to be the one browsers support natively.
How the DOM Tree Works
When the browser parses an HTML document, it builds a tree structure where every piece of content is represented as a node. This tree has a single root (the document object) and branches out into the <html>, <head>, <body>, and all their descendants.
The browser converts this into a tree:
document
└── html (lang="en")
├── head
│ └── title
│ └── "My Page" (text node)
└── body
├── h1
│ └── "Hello!" (text node)
└── p
└── "Welcome to the DOM." (text node)Key insight: Whitespace between tags (spaces, newlines, indentation) also creates Text nodes in the DOM. This is why childNodes often returns more nodes than you expect — it includes these invisible whitespace text nodes.
Types of DOM Nodes
Not everything in the DOM tree is an HTML element. The DOM distinguishes between several types of nodes, each with a numeric nodeType identifier:
| Node Type | nodeType | Description |
|---|---|---|
| Document | 9 | The root of the entire DOM tree. Represents the whole HTML or XML document. Accessed via the global document object. |
| Element | 1 | Represents an HTML tag like <div>, <p>, or <a>. Element nodes can have attributes and child nodes. |
| Text | 3 | The actual text content inside an element. For example, the words inside a <p> tag are a Text node. |
| Comment | 8 | Represents an HTML comment (<!-- ... -->). Comment nodes exist in the tree but are not rendered. |
| DocumentFragment | 11 | A lightweight container that holds a group of nodes. Useful for building subtrees in memory before inserting them into the live DOM. |
| Attr (Attribute) | 2 | Represents an attribute of an element (e.g. class="active"). In modern DOM, attributes are accessed via element methods rather than as separate nodes. |
Selecting Elements
Before you can change anything in the DOM, you first need to find it. These methods let you reach into the tree and grab one or more elements using IDs, CSS selectors, class names, or tag names.
| Method / Property | Description | Returns |
|---|---|---|
| document.getElementById(id) | Returns the single element with the matching id attribute. Returns null if no match is found. | Element | null |
| document.querySelector(selector) | Returns the first element matching a CSS selector. Extremely versatile — supports any valid CSS selector. | Element | null |
| document.querySelectorAll(selector) | Returns all elements matching a CSS selector as a static NodeList. Does not update when the document changes. | NodeList |
| document.getElementsByClassName(name) | Returns a live HTMLCollection of all elements with the given class name. Updates automatically when the DOM changes. | HTMLCollection |
| document.getElementsByTagName(tag) | Returns a live HTMLCollection of all elements with the given tag name (e.g. "div", "p"). | HTMLCollection |
Creating & Inserting
The DOM isn't read-only — you can build brand-new elements in JavaScript and inject them into the page. Elements created with createElement() exist only in memory until you explicitly attach them to the document tree.
| Method / Property | Description | Returns |
|---|---|---|
| document.createElement(tagName) | Creates a new HTML element in memory. The element is not part of the document until you insert it. | Element |
| document.createTextNode(text) | Creates a new Text node with the given string content. | Text |
| parent.appendChild(child) | Appends a node as the last child of a parent element. If the child already exists elsewhere, it is moved. | Node |
| parent.insertBefore(newNode, refNode) | Inserts a node before a reference child node. If refNode is null, the new node is appended at the end. | Node |
| element.append(...nodes) | Inserts nodes or strings after the last child. Unlike appendChild, it accepts multiple arguments and strings. | void |
| element.prepend(...nodes) | Inserts nodes or strings before the first child of the element. | void |
| element.after(...nodes) | Inserts nodes or strings immediately after the element itself (as siblings). | void |
| element.before(...nodes) | Inserts nodes or strings immediately before the element itself (as siblings). | void |
Modifying & Removing
Once elements are in the DOM, you can replace them, duplicate them, or remove them entirely. Removed nodes aren't destroyed immediately — they still exist in memory and can be re-inserted later if needed.
| Method / Property | Description | Returns |
|---|---|---|
| element.remove() | Removes the element from the DOM entirely. Simple and modern — no need to reference the parent. | void |
| parent.removeChild(child) | Removes a child node from its parent. Returns the removed node, which still exists in memory. | Node |
| parent.replaceChild(newChild, oldChild) | Replaces an existing child node with a new one. The old child is returned. | Node |
| element.replaceWith(...nodes) | Replaces the element with a set of nodes or strings. The element is removed from the tree. | void |
| element.cloneNode(deep) | Creates a copy of the element. If deep is true, all descendants are also cloned. | Node |
Attributes & Properties
HTML attributes (like class, id, data-*) and DOM properties (like classList, dataset) are how you inspect and configure elements. Attributes live in the HTML markup; properties are the JavaScript interface to those same values.
| Method / Property | Description | Returns |
|---|---|---|
| element.getAttribute(name) | Returns the value of the specified attribute, or null if it doesn't exist. | string | null |
| element.setAttribute(name, value) | Sets the value of an attribute on the element. Creates the attribute if it doesn't already exist. | void |
| element.removeAttribute(name) | Removes the specified attribute from the element. | void |
| element.hasAttribute(name) | Returns true if the element has the specified attribute, false otherwise. | boolean |
| element.classList | Returns a DOMTokenList for the class attribute. Provides add(), remove(), toggle(), contains(), and replace() methods. | DOMTokenList |
| element.dataset | Returns a DOMStringMap of all data-* attributes. Access data-user-id as element.dataset.userId. | DOMStringMap |
Content & Styling
These properties let you read or change what's inside an element — from plain text to full HTML markup — and modify its visual appearance directly through inline styles or by reading the browser's computed values.
| Method / Property | Description | Returns |
|---|---|---|
| element.textContent | Gets or sets the text content of an element and all its descendants. Does not parse HTML. | string |
| element.innerHTML | Gets or sets the HTML markup inside the element. Parses the string as HTML. Caution: can introduce XSS vulnerabilities. | string |
| element.outerHTML | Gets or sets the HTML including the element itself. Setting it replaces the element entirely. | string |
| element.style | Provides direct access to the inline CSS styles of the element (e.g. element.style.color = "red"). | CSSStyleDeclaration |
| getComputedStyle(element) | Returns the final computed CSS values of all properties on the element, after all stylesheets have been applied. | CSSStyleDeclaration |
DOM Traversal
Once you've selected an element, you can navigate to its relatives — parents, children, and siblings — using these properties:
| Property / Method | Description |
|---|---|
| parentNode | Returns the parent of the current node. For the document node, this is null. |
| parentElement | Returns the parent element. Unlike parentNode, returns null if the parent is not an Element (e.g. Document). |
| childNodes | Returns a live NodeList of all child nodes, including text and comment nodes. |
| children | Returns a live HTMLCollection of only the child elements (no text or comment nodes). |
| firstChild | Returns the first child node (could be text or whitespace). Use firstElementChild for the first element. |
| firstElementChild | Returns the first child that is an Element node, skipping text and comments. |
| lastChild | Returns the last child node. Use lastElementChild for the last element. |
| lastElementChild | Returns the last child that is an Element node. |
| nextSibling | Returns the next node at the same level (could be text). Use nextElementSibling for the next element. |
| nextElementSibling | Returns the next sibling that is an Element, skipping text and comment nodes. |
| previousSibling | Returns the previous node at the same level. Use previousElementSibling for the previous element. |
| previousElementSibling | Returns the previous sibling that is an Element. |
| closest(selector) | Walks up the DOM tree and returns the first ancestor (or self) matching the CSS selector. Returns null if none found. |
DOM Events System
Events are signals that something has happened — a click, a key press, the page finishing loading, or a form being submitted. The DOM event system lets you listen for and respond to these signals.
| Concept | Description |
|---|---|
| addEventListener() | The modern way to attach event handlers. Supports multiple listeners per event, capture/bubble phase control, and options like once and passive. |
| removeEventListener() | Removes an event listener previously registered with addEventListener. Requires the exact same function reference. |
| Event Bubbling | When an event occurs on an element, it first runs handlers on that element, then on its parent, then upward through the ancestors. Most events bubble by default. |
| Event Capturing | The opposite of bubbling — the event travels from the root down to the target. Enable capture by passing { capture: true } to addEventListener. |
| event.stopPropagation() | Prevents the event from bubbling up (or capturing down) to parent elements. Only the current listener runs. |
| event.preventDefault() | Prevents the browser's default behavior for the event (e.g. following a link, submitting a form, scrolling). |
| Event Delegation | Instead of attaching listeners to many child elements, attach a single listener to a common ancestor and use event.target to identify which child was clicked. More efficient and works for dynamically added elements. |
Practical Examples
NodeList vs HTMLCollection
DOM methods return different types of collections. Understanding the difference prevents subtle bugs:
| Feature | NodeList | HTMLCollection |
|---|---|---|
| Returned by | querySelectorAll(), childNodes | getElementsByClassName(), getElementsByTagName(), children |
| Live or Static? | Static (from querySelectorAll) — a snapshot that doesn't update | Live — automatically updates when the DOM changes |
| Contains | Any node type (elements, text, comments) | Only Element nodes |
| forEach support | ✅ Yes, has .forEach() | ❌ No — convert with Array.from() or [...collection] |
Performance Best Practices
Every time you modify the DOM, the browser may need to recalculate layouts (reflow) and repaint pixels on screen. These operations are expensive. Here's how to minimize their cost:
- Batch DOM changes: Use
DocumentFragmentor build an HTML string and setinnerHTMLonce, instead of inserting elements one by one. - Cache selectors: Store the result of
querySelector()in a variable instead of querying the DOM repeatedly in a loop. - Use event delegation: Attach one listener to a parent instead of attaching listeners to hundreds of child elements.
- Prefer classList over className: Using
classList.add()andclassList.remove()is more efficient and safer than overwritingclassNamedirectly. - Avoid layout thrashing: Don't alternate between reading layout properties (like
offsetHeight) and writing styles. Group all reads together, then all writes. - Use textContent over innerHTML: When you only need to set text,
textContentis faster and avoids HTML parsing and XSS risks. - Detach, modify, reattach: For complex modifications, remove the element from the DOM, make all changes, then reinsert it — triggering only one reflow.
DOM vs Virtual DOM
Modern JavaScript frameworks like React, Vue, and Preact use a concept called the Virtual DOM — a lightweight JavaScript copy of the real DOM tree. When your data changes, the framework first updates this virtual copy, then calculates the minimum number of changes needed, and finally applies only those changes to the real DOM.
This is called reconciliation or diffing. The benefit is that instead of replacing large chunks of the page, the framework surgically updates only what actually changed — making complex UIs fast and efficient.
Understanding the real DOM is essential even when using these frameworks, because the Virtual DOM is just an optimization layer on top of the same browser APIs covered on this page.
Elements in the DOM also support global attributes and event handler attributes. Browse our HTML Tags Reference to learn about individual elements.