What Is the DOM?
When a browser loads an HTML file it parses the markup and builds a tree of nodes. Each HTML element becomes an Element node, text content becomes a Text node, and comments become Comment nodes. The root of the tree is document.
The DOM is live β changes made via JavaScript are instantly reflected in the browser viewport and vice-versa. It is also language-agnostic; the DOM specification is maintained by the W3C/WHATWG independently of JavaScript, but JavaScript is its most common interface.
The DOM and the raw HTML source are not the same thing. The browser auto-corrects invalid HTML, JavaScript can modify the DOM after load, and browser extensions inject extra nodes β so the DOM you inspect in DevTools may differ from the original file.
Selecting Elements
Before you can manipulate an element you must obtain a reference to it. JavaScript provides multiple selection APIs:
| Method | Returns | Notes |
|---|---|---|
getElementById(id) | Element | null | Fastest for single ID lookup |
getElementsByClassName(cls) | HTMLCollection (live) | Updates automatically when DOM changes |
getElementsByTagName(tag) | HTMLCollection (live) | Pass "*" for all elements |
querySelector(selector) | Element | null | CSS selector, first match only |
querySelectorAll(selector) | NodeList (static) | All matches; not live |
// By ID (returns one element or null)
const title = document.getElementById('main-title');
// By class name (live HTMLCollection)
const cards = document.getElementsByClassName('card');
// CSS selector β first match
const firstBtn = document.querySelector('.btn-primary');
// CSS selector β all matches (static NodeList)
const allLinks = document.querySelectorAll('nav a');
allLinks.forEach(link => console.log(link.href));
// Scoped query β search inside an element
const nav = document.getElementById('main-nav');
const activeItem = nav.querySelector('.active');
Traversing the DOM
Once you have a reference you can navigate to related nodes using traversal properties:
const list = document.querySelector('ul');
// Parent
console.log(list.parentNode); // immediate parent node
console.log(list.parentElement); // same but always an Element
// Children (element-only, preferred)
console.log(list.children); // HTMLCollection of child Elements
console.log(list.firstElementChild); // first <li>
console.log(list.lastElementChild); // last <li>
// Siblings
const item = list.firstElementChild;
console.log(item.nextElementSibling); // second <li>
console.log(item.previousElementSibling); // null (it's first)
// All nodes including Text/Comment
console.log(list.childNodes); // NodeList including whitespace text nodes
console.log(list.firstChild); // often a text node (whitespace)
Prefer children, firstElementChild, nextElementSibling over childNodes, firstChild, nextSibling. The Node versions include whitespace text nodes and are rarely what you want.
Modifying Content
Three properties let you read and write element content. Each has different behaviour β choose deliberately:
const div = document.querySelector('#demo');
// innerHTML: parses HTML tags β powerful but XSS risk with user data
div.innerHTML = '<strong>Hello</strong> World';
// textContent: raw text, ignores tags β safe for user-supplied strings
div.textContent = '<script>alert(1)</script>'; // displays as text, not executed
// innerText: visible text only (respects CSS display:none, triggers reflow)
console.log(div.innerText); // reflects what user sees
// Differences at a glance:
// innerHTML β fastest for bulk HTML, dangerous with untrusted input
// textContentβ safe, no rendering cost for hidden elements
// innerText β CSS-aware, slower (triggers layout)
div.innerHTML β renders bold "Hello" + " World" div.textContent β displays literal <script> tag as text (safe)
Attributes and Dataset
HTML attributes are accessible through dedicated methods or the convenient dataset property for data-* attributes:
const img = document.querySelector('img');
// Standard attribute methods
console.log(img.getAttribute('src')); // current src value
img.setAttribute('alt', 'A scenic view'); // set or create
img.removeAttribute('loading'); // remove attribute
// Property shortcut (for reflected properties)
img.src = 'photo.jpg'; // same as setAttribute for src
img.alt = 'Photo';
// data-* attributes via dataset (camelCase conversion)
// HTML: <div data-user-id="42" data-role="admin">
const card = document.querySelector('[data-user-id]');
console.log(card.dataset.userId); // "42" (data-user-id β userId)
console.log(card.dataset.role); // "admin"
card.dataset.role = 'editor'; // updates attribute in DOM
Modifying Styles and Classes
Inline styles are set via the style property; class-based styling is managed with classList:
const box = document.querySelector('.box');
// Inline style (camelCase property names)
box.style.backgroundColor = '#3498db';
box.style.fontSize = '18px';
box.style.transform = 'translateX(20px)';
// Read computed style (includes stylesheet rules)
const computed = getComputedStyle(box);
console.log(computed.display); // "flex" (even if set by CSS file)
// classList API β preferred over className string manipulation
box.classList.add('highlight'); // add class
box.classList.remove('hidden'); // remove class
box.classList.toggle('active'); // add if absent, remove if present
box.classList.toggle('active', true); // force-add (2nd arg = boolean)
console.log(box.classList.contains('highlight')); // true
box.classList.replace('highlight', 'selected'); // swap classes
Creating and Inserting Elements
// createElement + appendChild (classic)
const li = document.createElement('li');
li.textContent = 'New item';
li.classList.add('list-item');
document.querySelector('ul').appendChild(li);
// prepend β insert as first child
const ul = document.querySelector('ul');
const firstItem = document.createElement('li');
firstItem.textContent = 'First!';
ul.prepend(firstItem);
// insertBefore(newNode, referenceNode)
const ref = ul.children[1];
const inserted = document.createElement('li');
inserted.textContent = 'Inserted before index 1';
ul.insertBefore(inserted, ref);
// insertAdjacentHTML β fastest for raw HTML strings
// positions: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend'
ul.insertAdjacentHTML('beforeend', '<li class="list-item">Last via HTML</li>');
// cloneNode(deep) β duplicate an element
const clone = li.cloneNode(true); // true = deep clone (includes children)
ul.appendChild(clone);
Resulting list: β’ First! β’ New item (original) β’ Inserted before index 1 β’ (original index 1 item) β’ ... β’ Last via HTML
Removing Elements
// Modern: element.remove() β supported in all modern browsers
const obsolete = document.getElementById('old-banner');
obsolete.remove();
// Legacy: parentNode.removeChild(child)
const item = document.querySelector('.to-delete');
item.parentNode.removeChild(item);
// Remove all children efficiently
const container = document.querySelector('#results');
container.innerHTML = ''; // fast but may leak event listeners
// Alternative (preserves listeners on children if any):
while (container.firstChild) {
container.removeChild(container.firstChild);
}
Never insert raw user-supplied strings using innerHTML. An attacker can inject <script> tags or event handlers. Always use textContent for user data, or sanitize with a library like DOMPurify.
Practical Exercise
ποΈ Practical Exercise
Build a dynamic to-do list using only DOM APIs:
- Create an
<input>and an<ul>in your HTML. - When the user presses Enter in the input, create a new
<li>with the input's text and append it to the list. - Each
<li>should have a "Delete"<button>; clicking it calls.remove()on the parent<li>. - Clicking the
<li>text toggles adoneCSS class that appliestext-decoration: line-through.
π₯ Challenge Exercise
Extend the to-do list with drag-and-drop reordering using only the draggable attribute and dragstart / dragover / drop events. Store the dragged element reference and use insertBefore to reorder items on drop.
Summary
π Summary
- The DOM is a live tree of nodes representing the parsed HTML page.
- Use
querySelector/querySelectorAllfor flexible CSS-selector-based selection. - Traverse with
children,firstElementChild,nextElementSibling(element-only variants). - Modify content with
textContent(safe) orinnerHTML(powerful, XSS risk). - Manage attributes via
getAttribute/setAttributeanddata-*viadataset. - Prefer
classListover manipulatingclassNamestrings directly. - Create nodes with
createElement, insert withappend/prepend/insertAdjacentHTML. - Remove nodes with
element.remove()(modern) orparent.removeChild()(legacy).
Interview Questions
- What is the difference between
innerHTML,textContent, andinnerText? - What does "the DOM is live" mean? How does a live HTMLCollection differ from a static NodeList?
- How would you efficiently remove all child elements from a container?
- What is the difference between an HTML attribute and a DOM property?
- Why is inserting user-supplied strings with
innerHTMLdangerous?
Frequently Asked Questions
querySelector and getElementById? +getElementById is slightly faster because it uses the browser's internal ID index. querySelector accepts any CSS selector so it is far more flexible. For a single ID lookup either works; for anything more complex use querySelector.
If your <script> is in <head> without defer or async, the DOM is not yet built and querySelector will return null. Place scripts at the bottom of <body>, use the defer attribute, or wrap code in a DOMContentLoaded event listener.
insertAdjacentHTML and when should I use it? +insertAdjacentHTML parses an HTML string and inserts nodes at a specified position relative to an element without replacing existing content. It is faster than setting innerHTML because it does not re-parse the entire element β ideal for appending rows to a table or items to a list.