The DOM: What the Browser Builds From Your Markup
Your file is text. The DOM is the live tree the browser built from it, and they are not the same thing. Knowing the difference is what makes debugging possible.
-
HTML Basics
- What is HTML: The Structure Layer of Every Web Page
- HTML Document Structure: DOCTYPE, html, head and body
- Elements, Tags and Attributes: The Vocabulary of HTML
- HTML Comments: Notes That Ship With Your Code
- Block Level and Inline Elements
- Writing and Running Your First HTML Page
- How a Browser Turns Markup Into a Page
- Text and Formatting
- Links and Navigation
- Images and Media
- Lists
- Tables
-
Forms
- Form Structure: form, action and method
- Input Types: Text, Email, Number, Date and the Rest
- Labels: The Most Important Element in a Form
- Checkboxes, Radio Buttons and Grouping
- select, option, optgroup and datalist
- textarea, File Uploads and Hidden Fields
- Buttons: submit, reset and button
- Built In Form Validation
- GET or POST: What Happens When a Form Is Submitted
- Semantic HTML
- HTML5 Features
- Head and Metadata
- HTML with CSS
- HTML with JavaScript
- Accessibility
-
HTML SEO
- How Google Works: Crawling, Indexing and Ranking
- SEO Friendly HTML Structure
- Titles and Descriptions That Earn Clicks
- Headings and Content Structure for Search
- Internal Linking and Anchor Text
- robots.txt and XML Sitemaps
- Canonical URLs and Duplicate Content
- Structured Data and JSON-LD
- Image SEO
- Core Web Vitals and Mobile Friendliness
- DevTools and Debugging
- Editor Productivity
- HTML Best Practices
- HTML Projects
- Advanced Projects
- Practice and Exams
Concept
The Document Object Model is the browser representation of your page: a tree of objects, one per element, that JavaScript can read and change.
The single most useful thing to understand about it is that the DOM is not your file. The file is text on disk. The DOM is what the parser built from it - after repairing errors, after inserting implied elements, and after every script has finished modifying it.
The tree
<!DOCTYPE html>
<html lang="en">
<head>
<title>Courses</title>
</head>
<body>
<h1>Courses</h1>
<p>We offer <strong>three</strong> courses.</p>
</body>
</html>document
└── html (lang="en")
├── head
│ └── title
│ └── "Courses"
└── body
├── h1
│ └── "Courses"
└── p
├── "We offer "
├── strong
│ └── "three"
└── " courses."Every element is a node. So is every run of text, and so is every comment. Which is why childNodes often returns more than you expect - the whitespace between tags is text nodes too.
document.body.childNodes.length; // includes whitespace text nodes
document.body.children.length; // elements only - usually what you wantWhere the DOM differs from your file
The parser repairs errors
<p>Total: <div>1,200</div></p>A paragraph cannot contain a div, so the parser closes the paragraph first, leaves the div as a sibling, and discards the orphaned closing tag. Three nodes, none of them what you wrote.
Implied elements are inserted
Loose table rows get a tbody around them. A missing head or body is invented. These appear in the DOM without appearing in your file.
Scripts change it
Anything a script adds, removes or edits is in the DOM and not in the file.
The practical consequence: View source shows the file the server sent. The Elements panel shows the live DOM. When they disagree, the Elements panel is the truth, and debugging against view source is why some bugs seem impossible.
Navigating the tree
const p = document.querySelector("p");
p.parentElement; // the containing element
p.children; // element children only
p.firstElementChild;
p.lastElementChild;
p.nextElementSibling;
p.previousElementSibling;
p.closest("article"); // nearest ancestor matching a selector
p.matches(".lead"); // does this element match?
p.contains(other); // is other inside p?closest is the one to remember. It walks up from an element until it finds a match, and it is what makes event delegation clean.
Reading and writing content
el.textContent // all text, including hidden elements. Safe.
el.innerText // rendered text only, respects display:none. Triggers layout.
el.innerHTML // markup as a string. Parses anything you assign.
el.outerHTML // including the element itselfThe distinction matters for security:
const name = getUserInput();
el.textContent = name; // inserted as text, always safe
el.innerHTML = name; // parsed as markup - an injection pointAssigning untrusted input to innerHTML is the standard cross site scripting vector. textContent is both safer and faster; use it unless you are deliberately inserting markup you control.
Attributes and properties
// attributes: what is in the markup
el.getAttribute("value");
el.setAttribute("value", "abc");
el.hasAttribute("required");
el.removeAttribute("disabled");
// properties: the live state of the object
input.value; // what the user has typed now
input.checked;
input.disabled;
// classes
el.classList.add("is-open");
el.classList.remove("is-open");
el.classList.toggle("is-open");
el.classList.contains("is-open");
// data attributes
el.dataset.status;The difference bites on form fields. getAttribute("value") returns the value written in the markup - the initial value. input.value returns what the reader has typed. They diverge the moment anyone types anything.
Creating and inserting
const item = document.createElement("li");
item.textContent = "Design";
item.className = "course";
list.append(item); // at the end
list.prepend(item); // at the start
item.before(other); // as a preceding sibling
item.after(other); // as a following sibling
item.replaceWith(other);
item.remove();
list.replaceChildren(a, b, c); // replace all children at onceBuilding many nodes into a fragment first means one insertion and one layout pass rather than one per node:
const fragment = document.createDocumentFragment();
for (const course of courses) {
const li = document.createElement("li");
li.textContent = course.title;
fragment.append(li);
}
list.replaceChildren(fragment);Reflow and repaint
Changing the DOM can force the browser to recalculate layout. Reading a layout value immediately after writing one forces it synchronously, and doing that inside a loop is a well known performance trap:
// forces layout on every iteration
items.forEach((el) => {
el.style.width = el.offsetWidth + 10 + "px";
});
// read everything, then write everything
const widths = items.map((el) => el.offsetWidth);
items.forEach((el, i) => { el.style.width = widths[i] + 10 + "px"; });Important rules
- The DOM is built from the parsed result, not from your source text.
childNodesincludes text and comment nodes;childrendoes not.textContentis safe;innerHTMLparses markup.- Attributes are the markup; properties are the live state.
- A script cannot reach an element that has not been parsed yet - which is what
defersolves. - Every DOM change is a potential layout and paint.
Common mistakes
- Debugging against view source instead of the Elements panel.
- Assigning user input to
innerHTML. - Using
getAttribute("value")to read what someone typed. - Iterating
childNodesand tripping over whitespace text nodes. - Using
innerHTML +=, which reparses the container and destroys event listeners. - Inserting nodes one at a time in a loop.
- Interleaving reads and writes of layout values.
Best practices
- Use
textContentfor text andcreateElementortemplatefor structure. - Use
children, notchildNodes. - Use
closestfor walking upwards. - Batch insertions with a fragment.
- Read layout values before writing, never alternately.
- Trust the Elements panel over the page source.
Practice
- Write
<p>Total: <div>1,200</div></p>, then compare view source with the Elements panel and draw both trees. - Log
childNodes.lengthandchildren.lengthfor a container with indented markup, and explain the difference. - Set a value containing markup with
textContentand then withinnerHTML. - Insert two hundred list items one at a time and then via a fragment, and compare the timings.