The DOM (Document Object Model)
Learn what the DOM actually is, how to select elements with getElementById, querySelector and querySelectorAll, and how to read, write, and create HTML content from JavaScript.
Reading Time
18 min
Lesson
Lesson 33 of 48
What is the DOM?
The DOM, short for Document Object Model, is the browser's live, in-memory representation of the HTML page as a tree of objects. When the browser loads an HTML file, it doesn't just display the text — it parses that text into a tree of nodes, where each element, attribute, and piece of text becomes an object you can inspect and manipulate with JavaScript. This is why the DOM matters so much: it's the bridge between the static HTML you write and the dynamic, interactive page the user actually sees. Change the DOM, and the browser repaints the page to match, immediately.
The DOM is Not HTML
It's easy to think of the DOM as just "the HTML", but they're different things. HTML is the text file the server sends down; the DOM is the object structure the browser builds from that text after parsing it. The DOM can also be changed after the page loads — by JavaScript, by browser extensions, even by the browser itself correcting malformed markup — so at any given moment, the DOM might look very different from the original HTML source. The document object is your entry point into this tree; everything else is reached by starting there.
The document Object
Console Output
Click “Run” to see the console output here.
Selecting Elements
Before you can read or change anything on the page, you need to select the element (or elements) you're interested in. getElementById() finds a single element by its id attribute and is the fastest lookup, since ids are supposed to be unique. querySelector() is more flexible — it accepts any CSS selector and returns the first matching element, or null if nothing matches. querySelectorAll() also accepts a CSS selector, but returns every match, not just the first one.
getElementById, querySelector, querySelectorAll
Console Output
Click “Run” to see the console output here.
NodeList is Not an Array
querySelectorAll() returns a NodeList, not a real array. A NodeList does support forEach(), which covers most everyday needs, but it doesn't have map(), filter(), reduce(), or the other array methods you might reach for. If you need those, convert it first with Array.from(nodeList) or [...nodeList]. getElementsByClassName() and getElementsByTagName() are older alternatives that return a live HTMLCollection instead, which updates automatically as the DOM changes — a subtle difference from the static NodeList that querySelectorAll() returns.
Reading and Writing Content: textContent vs. innerHTML
Once you have an element, textContent lets you read or write its text, treating everything as plain text — any HTML-looking string you assign to it is inserted literally, tags and all, rather than being parsed. innerHTML, by contrast, reads or writes the actual HTML markup inside an element, parsing whatever string you assign into real elements. That parsing is exactly what makes innerHTML risky: if the string ever comes from user input rather than a value you fully control, an attacker can inject a <script> or an event-handler attribute that runs in your page's context — a classic cross-site scripting (XSS) vulnerability. Assigning untrusted text should almost always go through textContent instead.
textContent vs. innerHTML
Console Output
Click “Run” to see the console output here.
Attributes and Classes
HTML attributes — like src, href, or data-* custom attributes — are read and written with getAttribute() and setAttribute(). For the class attribute specifically, the classList property is usually more convenient than working with the raw string: it exposes add(), remove(), toggle(), and contains() methods that let you manage individual classes without accidentally clobbering the others already on the element.
Attributes and classList
Console Output
Click “Run” to see the console output here.
Creating and Inserting Elements
To add brand-new content to the page, you first build the element in memory with document.createElement(), configure it (set its text, attributes, classes), and then attach it to the visible tree with a method like appendChild() or the newer append(). Nothing appears on screen until that last insertion step — creating an element on its own just produces a detached object floating in memory. remove() takes an element back out of the DOM entirely.
createElement, appendChild, append, remove
Console Output
Click “Run” to see the console output here.
Practical Example: Rendering a List of Items
Console Output
Click “Run” to see the console output here.
Core DOM Operations at a Glance
- Select: getElementById(), querySelector(), querySelectorAll()
- Read/write text: textContent (safe), innerHTML (parses markup, risky with untrusted input)
- Attributes: getAttribute(), setAttribute()
- Classes: classList.add(), remove(), toggle(), contains()
- Create and insert: createElement(), appendChild(), append()
- Remove: element.remove()
Never Pipe Untrusted Input into innerHTML
Any time the string you assign to innerHTML could contain user-supplied content — a comment, a search query, a profile bio — you are opening the door to XSS unless that content is sanitized first. Prefer textContent for plain text, or a trusted sanitization library if you genuinely need to render user-supplied HTML.
Batch Your DOM Changes
Every insertion into the live DOM can trigger the browser to recalculate layout and repaint the page, which is comparatively expensive. When building up a list like the fruit example above, it's often faster to build the elements first and append them all at once (or use a DocumentFragment) rather than touching the live DOM inside a tight loop, especially for large lists.