The DOM — Document Object Model
Status: 🟩 COMPLETE Last updated: 2026-06-20 Plain-English tagline: The live tree of HTML elements the browser builds when it loads a page. JavaScript reads from it, writes to it. React diffs against it. Without the DOM, the web is static.
In plain English
When a browser receives HTML, it parses the text into a structure called the Document Object Model, or DOM. The DOM is a tree of objects, one for each element on the page. JavaScript can read from the tree (find an element, get its text, check its size) and write to it (change text, add new elements, remove old ones).
If you’ve ever opened DevTools and seen the “Elements” panel showing your page’s HTML in an inspectable tree — that’s the DOM, rendered for human inspection.
The actual HTML you write is text. The DOM is the in-memory data structure built from that text. The two are related but distinct. After the page loads, the DOM is what really matters — change the DOM and the page updates. Change the original HTML text and nothing happens (it’s already been parsed).
Why it matters
- All web interactivity flows through the DOM. Every click handler, every form input, every animation — JavaScript reads or modifies the DOM.
- React’s whole job is updating the DOM efficiently. Understanding what React is doing requires knowing what the DOM is.
- Debugging UI issues means reading the DOM in DevTools, not the source HTML.
- Performance issues often trace to “too many DOM updates” or “expensive DOM queries.”
You won’t manipulate the DOM directly very often if you use React. But the concepts underlie everything.
The tree structure
For HTML like:
<body>
<header>
<h1>Title</h1>
</header>
<main>
<p>Hello <strong>world</strong></p>
</main>
</body>The DOM tree is:
body
├── header
│ └── h1
│ └── "Title" (text node)
└── main
└── p
├── "Hello " (text node)
└── strong
└── "world" (text node)
Each box is a node. Nodes have parents, children, siblings. Text is a special kind of node (“text node”); elements are another kind (“element node”).
There are also attribute nodes, comment nodes, and others — but for day-to-day work, “elements” and “text” cover most of what you care about.
How JavaScript talks to the DOM
The browser provides a global document object. From there, you can find, read, and modify anything:
// Find an element
const title = document.querySelector("h1");
const allParagraphs = document.querySelectorAll("p");
const byId = document.getElementById("main-title");
// Read
console.log(title.textContent); // "Title"
console.log(title.tagName); // "H1"
console.log(title.getAttribute("class")); // "..."
// Modify
title.textContent = "New title";
title.classList.add("highlighted");
title.style.color = "red";
// Create new elements
const newP = document.createElement("p");
newP.textContent = "I'm new here";
document.body.appendChild(newP);
// Remove
title.remove();These APIs are old (15+ years), verbose, and clunky. Modern frameworks (React, Vue, Svelte) wrap them so you rarely call them directly. But they’re what’s underneath.
How React relates to the DOM
When you write a React component:
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}React doesn’t directly manipulate the DOM. Instead:
- React calls your component function, gets back a JSX tree.
- React maintains a virtual DOM — an in-memory representation of what the DOM should look like.
- When state changes, React calls your function again, gets a new JSX tree.
- React diffs the new tree against the previous one.
- React applies the minimum DOM updates needed to make the real DOM match.
This indirection is why React is fast. You don’t manually manage updates; React batches and minimizes them.
The “virtual DOM” terminology is somewhat dated — React Compiler in 2026 does more than a simple diff — but the model is still useful.
Selecting elements
The modern, standard methods:
| Method | Returns |
|---|---|
document.querySelector(css) | First matching element or null |
document.querySelectorAll(css) | NodeList of all matches |
document.getElementById(id) | Element with that ID or null |
element.querySelector(css) | Search within a specific element |
element.closest(css) | Walk up the tree to find a matching ancestor |
The css argument is any CSS selector — same syntax as in stylesheets. Powerful.
Reading and writing properties
Some common ones:
element.textContent // string contents (all text inside)
element.innerHTML // HTML inside (CAREFUL — XSS risk if user-input)
element.value // value of input/textarea/select
element.checked // boolean for checkboxes/radios
element.classList // add/remove/toggle classes
element.style // inline styles
element.dataset // data-* attributes
element.getAttribute(name)
element.setAttribute(name, value)
element.parentElement // immediate parent
element.children // child elements (not text nodes)
element.nextElementSibling // next sibling element
element.scrollTop // scroll position
element.offsetWidth // sizeEvents
The DOM dispatches events when things happen: clicks, key presses, scrolls, network responses. You listen with addEventListener:
const button = document.querySelector("#save");
button.addEventListener("click", (event) => {
console.log("Saved!", event);
});The event object contains details (which element, mouse position, modifier keys, etc.).
In React, you write onClick={...} directly on JSX elements — React wires up the listeners for you.
Event bubbling
When you click on a deeply-nested element, the event “bubbles up” — fires on the clicked element, then its parent, then its parent, all the way to document. You can listen at any level. Used heavily for “delegate handler on parent, identify target child” patterns.
event.stopPropagation() stops the bubble.
Hydration (Next.js / React + SSR)
When Next.js server-renders a page, it generates HTML and sends it to the browser. The browser immediately parses the HTML and builds the DOM. The page is visible.
Then React loads and hydrates — it traverses the DOM, attaches event handlers, and “takes over” the page. From that point on, it behaves like a normal React app.
For hydration to succeed, the HTML React generates on the client must exactly match the HTML the server sent. Mismatches (caused by Date.now(), random values, or browser-only APIs in components that render on both sides) produce errors and possibly broken interactivity.
This is one of the most common sources of “weird React bugs” in Next.js — solvable but persistent.
A concrete example: highlighting text
Without React (vanilla):
const heading = document.querySelector("h1");
heading.style.background = "yellow";
heading.style.padding = "8px";
heading.addEventListener("click", () => {
heading.style.background = "lime";
});With React:
function Highlightable({ text }: { text: string }) {
const [highlighted, setHighlighted] = useState(false);
return (
<h1
onClick={() => setHighlighted(true)}
style={{
background: highlighted ? "lime" : "yellow",
padding: 8
}}
>
{text}
</h1>
);
}Same behavior. React describes what the DOM should look like for each state; the framework handles the actual mutations.
When you reach for direct DOM access in React
Even with React, you sometimes need raw DOM access:
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
}The ref gives you the underlying DOM node. Use it for:
- Focus management (
.focus()) - Measuring (
.offsetWidth,.getBoundingClientRect()) - Animation libraries that need element refs
- Integrating non-React libraries (charting, mapping, etc.)
Otherwise, stay in React’s declarative model.
Common gotchas
-
innerHTMLis dangerous with user input. Settingelement.innerHTML = userTextexecutes any<script>tags or event-handler attributes in the input — that’s XSS. UsetextContentfor user input. In React,dangerouslySetInnerHTMLis the explicit (and ugly) name precisely because of this. -
querySelectorAllreturns a NodeList, not an Array. Some array methods work (.forEach()); others (.map(),.filter()) don’t. Convert withArray.from(nodeList)if needed. -
DOM changes trigger re-layout. Reading certain properties (
offsetWidth,getBoundingClientRect) forces the browser to recompute layout. Doing this in a loop is slow. -
textContentvsinnerTextvsinnerHTML. Each is subtly different.textContentis fastest and safest.innerTextis layout-aware (respects display:none).innerHTMLincludes the tags. -
Events bubble; some don’t. Most do. A few (focus, blur, mouseenter, mouseleave) don’t. Use
focusin/focusoutandmouseover/mouseoutif you need bubbling. -
document.querySelectorfinds only ONE. Easy to forget when chaining. UsequerySelectorAllif you need all matches. -
CSS selectors in JS are strings. Typos don’t error — they silently match nothing. Test in DevTools console first.
-
DOM is not the same as the source HTML. The browser fixes invalid HTML when parsing (closes unclosed tags, reorders things). The DOM may differ from your source.
-
React doesn’t see direct DOM mutations. If you change the DOM outside React (e.g. a third-party library), React doesn’t know. Re-renders will overwrite. Use refs and effects to manage handoff.
-
Hydration mismatches are common. Anything time-dependent or randomness-dependent during render produces mismatches between server and client. Wrap in
useEffectfor client-only behavior. -
style.color = "blue"works;style.background-color = ...doesn’t. JS uses camelCase for hyphenated properties:backgroundColor. Or usesetProperty().
See also
- HTML 🟩 — what gets parsed into the DOM
- JavaScript 🟩 — the language that manipulates it
- CSS 🟩 — styling the DOM
- React 🟩 — DOM updates done declaratively
- Next.js 🟩 🟦 — server-rendered, hydrated DOM
- Accessibility (a11y) 🟩 — DOM structure affects screen readers
- XSS 🟩 — the security risk of careless DOM modification
- Glossary: DOM, Hydration