JavaScript Introduction to the DOM
The DOM (Document Object Model) is the browser’s live, in-memory representation of your HTML page as a tree of objects. It is the bridge between the static HTML you write and the dynamic page a user sees, and it’s what JavaScript actually talks to when it reads or changes anything on screen. Every button click handler, every dynamically-updated price, every image slider you’ve ever used relies on the DOM underneath. Understanding it is the single most important step toward writing real browser JavaScript.
Overview / How the DOM Works
When a browser loads a page, it does not keep your HTML as a flat string of text. Instead, it parses the markup and builds a tree data structure in memory, where every tag, piece of text, comment, and even the document itself becomes an object called a node. This tree is the DOM. The outermost node is the document node, its child is the <html> element, which has children <head> and <body>, and so on down to the smallest text node inside a paragraph.
Crucially, the DOM is not the same thing as your original HTML source. The browser corrects malformed markup (auto-closing unclosed tags, moving misplaced elements), and once JavaScript starts modifying the page, the DOM can diverge completely from the file that was downloaded. If you view the DOM in DevTools after some JavaScript has run, it may look nothing like "View Source", because View Source shows the original bytes while DevTools shows the live tree.
Every node has a nodeType. The ones you’ll meet constantly are: 1 for element nodes (like <li>), 3 for text nodes (the words inside an element), 8 for comment nodes, and 9 for the document node itself. JavaScript in the browser is handed two global objects that give it access to this world: window, which represents the browser tab/window and holds things like alert, setTimeout, and location; and window.document (usually just written document), which is the entry point into the page’s DOM tree.
The DOM API itself is intentionally language-agnostic — it was designed as a standard that any language could implement bindings for — but in a browser it’s JavaScript that exposes it to you as methods and properties on objects like document and individual elements. When you call document.querySelector(...), you are asking the engine to walk the live tree and hand you back a reference to a real node, not a copy. Change a property on that reference, and the change appears on the page immediately, because you’re holding a pointer into the actual structure the renderer reads from.
Syntax
There’s no single "DOM syntax" the way there is for a loop, but there is a general shape you’ll use constantly: select something, then read or write a property on it.
// selecting
const el = document.querySelector(selector);
// reading
const text = el.textContent;
// writing
el.textContent = 'new value';
el.style.color = 'red';
el.classList.add('active');
| Method / Property | Purpose |
|---|---|
document.getElementById(id) |
Finds one element by its exact id attribute |
document.querySelector(css) |
Finds the first element matching a CSS selector |
document.querySelectorAll(css) |
Finds all matching elements (static NodeList) |
document.getElementsByClassName(name) |
Finds elements by class (live HTMLCollection) |
el.textContent |
Gets/sets the plain-text content of an element |
el.innerHTML |
Gets/sets content parsed as HTML markup |
el.classList |
Add, remove, toggle, or check CSS classes |
el.parentNode / el.children |
Move up or down the tree |
el.nextElementSibling |
The next element at the same tree level |
Examples
Assume the page contains this markup for every example below:
<h1 id="title">Welcome to the DOM</h1>
<ul id="fruits">
<li class="fruit">Apple</li>
<li class="fruit">Banana</li>
<li class="fruit">Cherry</li>
</ul>
Example 1: Selecting elements and reading properties
const heading = document.getElementById('title');
console.log(heading.textContent);
console.log(heading.nodeName);
console.log(heading.nodeType);
const fruitList = document.querySelectorAll('.fruit');
console.log(fruitList.length);
console.log(fruitList[0].textContent);
Output:
Welcome to the DOM
H1
1
3
Apple
This shows the two most common lookup tools: getElementById for a single, uniquely-identified element, and querySelectorAll for a collection matched by CSS selector. nodeName returns the tag name in uppercase (H1), and nodeType of 1 confirms it’s an element node.
Example 2: Traversing the tree
const list = document.getElementById('fruits');
console.log(list.children.length);
console.log(list.firstElementChild.textContent);
console.log(list.firstElementChild.nextElementSibling.textContent);
console.log(list.parentNode.nodeName);
Output:
3
Apple
Banana
BODY
Once you have one node, you can walk to related nodes without any new lookup: children gives element children only (skipping text nodes made of whitespace), firstElementChild/nextElementSibling move sideways and down, and parentNode moves up. This is how you navigate relative to something you already found, instead of re-querying the whole document.
Example 3: Creating and modifying nodes
const list = document.getElementById('fruits');
const newFruit = document.createElement('li');
newFruit.textContent = 'Dragonfruit';
newFruit.className = 'fruit';
list.appendChild(newFruit);
console.log(list.children.length);
console.log(list.lastElementChild.textContent);
const heading = document.getElementById('title');
heading.style.color = 'blue';
console.log(heading.style.color);
Output:
4
Dragonfruit
blue
This is the DOM’s other half: not just reading the tree, but changing it. document.createElement makes a brand-new, detached node that exists only in memory; nothing happens on screen until you attach it somewhere with a method like appendChild. Setting el.style.color writes directly to that element’s inline style, which the renderer picks up on its next paint.
How It Works Step by Step (Under the Hood)
1. The browser receives raw HTML bytes over the network. 2. An HTML tokenizer breaks the byte stream into tokens (start tags, end tags, text, comments) as it streams in. 3. A tree construction step consumes those tokens and builds the DOM, creating a node object for each tag and inserting it as a child of whatever is currently open — this is also where malformed HTML gets silently corrected. 4. Once the initial HTML has been fully parsed, the browser fires the DOMContentLoaded event on document, signaling that the tree is ready to be queried and manipulated safely. 5. In parallel, the browser also parses your CSS into the CSSOM (CSS Object Model); the DOM and CSSOM are combined into a render tree, which is what actually gets painted to the screen. 6. Any later JavaScript that calls something like appendChild or sets textContent mutates the live DOM tree directly; the browser then recalculates layout (reflow) and repaints the affected pixels. Because step 6 can happen at any time, after page load, in response to a click, on a timer, forever — the DOM is not a one-time snapshot. It’s a living structure your script can read and rewrite for as long as the page is open.
Common Mistakes
Mistake 1: Querying elements before the DOM is ready. If a <script> tag sits in <head> without defer, it runs before <body> has been parsed, so lookups return null.
// script runs too early, before has been parsed
const list = document.getElementById('fruits');
console.log(list.children.length);
Output:
Throws: TypeError: Cannot read properties of null (reading 'children')
Fix it by waiting for the DOM to finish loading, or by placing scripts at the end of <body>, or by adding the defer attribute to the <script> tag:
document.addEventListener('DOMContentLoaded', () => {
const list = document.getElementById('fruits');
console.log(list.children.length);
});
Mistake 2: Mutating a live collection while iterating it. getElementsByClassName and getElementsByTagName return a live HTMLCollection that automatically shrinks as elements are removed, which silently skips items in a plain for loop.
const items = document.getElementsByClassName('fruit');
for (let i = 0; i < items.length; i++) {
items[i].remove();
}
console.log(items.length);
Output:
1
Only two of the three .fruit elements actually get removed, because the collection re-indexes itself after every removal. Use querySelectorAll instead, which returns a static NodeList that won't change out from under you, and iterate with forEach:
const items = document.querySelectorAll('.fruit');
items.forEach(item => item.remove());
console.log(document.getElementsByClassName('fruit').length);
Output:
0
Best Practices
- Prefer
textContentoverinnerHTMLwhen inserting plain text or user-supplied data, sinceinnerHTMLparses its argument as HTML and can open the door to XSS if the content isn't trusted. - Cache DOM references you'll use more than once in a variable instead of re-querying the tree every time — each lookup walks the tree and costs time.
- Prefer
querySelector/querySelectorAllover the oldergetElementsBy*family for consistency: they accept full CSS selectors and return static, predictable results. - Wait for
DOMContentLoaded(or use thedeferattribute on your<script>tag) before touching the DOM, rather than relying on script placement alone. - Batch DOM writes together where possible; reading a layout property (like
offsetHeight) right after writing one forces the browser to reflow early, which is expensive if repeated in a loop. - Use
classList.add/remove/toggleinstead of hand-editing theclassNamestring, so you don't accidentally clobber other classes already on the element.
Practice Exercises
- Given a page with an element
<p id="msg">Hello</p>, write JavaScript that selects it and changes its text to"Goodbye"usingtextContent. - Given a
<ul id="list">with three<li>children, write JavaScript that creates a fourth<li>with the text"New Item"and appends it, then logs how many children the list has afterward. - Write JavaScript that selects every element with the class
highlightusingquerySelectorAll, and adds the CSS classdoneto each one usingclassList.add.
Summary
- The DOM is the browser's in-memory tree representation of an HTML document, built by parsing HTML into node objects.
- The
documentobject is the entry point JavaScript uses to read and modify that tree;windowis the broader browser-tab object it lives on. - Common node types are element (
1), text (3), comment (8), and document (9). getElementById,querySelector, andquerySelectorAllare the main ways to find elements; properties liketextContent,children, andparentNodelet you read and traverse.createElementplusappendChild(or similar) is how new nodes are built and inserted into the live page.- Live collections (
getElementsByClassName) can silently skip items when mutated during iteration; staticNodeLists fromquerySelectorAllavoid that trap. - Always ensure the DOM has finished parsing (
DOMContentLoadedordefer) before your script tries to query it.
