Browser APIs

The DOM

The Document Object Model is the bridge between your HTML and your JavaScript. Learn how to select, create, update and remove elements, and how to respond to what users do.

beginner15 min readUpdated Sep 15, 2026
js
const list = document.querySelector("#todos");
const input = document.querySelector("#todo-input");

function addTodo(text) {
  const li = document.createElement("li");
  li.textContent = text;
  list.append(li);
}

input.addEventListener("keydown", (event) => {
  if (event.key === "Enter") {
    addTodo(input.value);
    input.value = "";
  }
});
What it is
A live tree of objects
Built by
The browser, from your HTML
Select with
querySelector / querySelectorAll
React with
addEventListener
Create with
document.createElement
Cost
Layout and paint work

Hands-on

Try DOM manipulation

Click the button, then edit the code to see how the tree changes.

Try DOM manipulation

Click the button, then edit the code to see how the tree changes.

Why it matters

Why the DOM matters

Your HTML, as objects

The browser parses markup into a tree of nodes your code can read and change at any time.

Instant feedback

Update the tree and the browser repaints automatically — no page reload, no server round trip.

React to anything

Clicks, typing, scrolling, resizing, network events and more all arrive through a single event model.

The big picture

The three ideas behind the DOM

Every DOM task is a combination of the same three moves: find something, change it, or react to it.

The tree

Structure

A hierarchy of nodes where every element, attribute and piece of text has a place and a parent.

Selection

Find & change

Query the tree, then read or write text, attributes, classes and styles on the nodes you found.

Events

React

Listen for user and browser events, then run code in response — ideally with delegation.

DOM at a glance

What you can do with the DOM

Selecting elements

querySelector returns the first match, querySelectorAll returns every match as a static list.

Reading & writing

textContent, innerHTML, attributes and dataset let you inspect and update content.

Classes & styles

classList and the style property change appearance without touching a stylesheet.

Building nodes

createElement, append, prepend and remove grow or shrink the tree on demand.

Events

addEventListener subscribes to clicks, input, keyboard, scroll and much more.

Event delegation

One listener on a parent can handle events from all its children, even ones added later.

A short history

How the DOM grew up

  1. 1996

    The first DOM

    Netscape and Microsoft expose competing ways to script pages, starting the first browser war.

    96
  2. 1998

    DOM Level 1

    The W3C standardises a common tree model and a shared API for manipulating it.

    98
  3. 2009

    querySelector arrives

    CSS-style selectors make finding elements dramatically simpler and more expressive.

    09
  4. 2015

    classList and modern APIs

    classList, dataset and template simplify everyday DOM work and replace old hacks.

    15
  5. Today

    A living standard

    The DOM is maintained as a continuously updated specification with broad browser agreement.

    Today

The complete guide

The DOM: Everything you need to know

What is the DOM?

When the browser reads your HTML, it does not keep a flat string of text. It builds a tree of objects called the Document Object Model. Every element becomes a node with properties and methods, every attribute is accessible, and every piece of text has a place. The DOM is that tree, and it is live: change it and the page updates.

This is why JavaScript and HTML feel connected. Your HTML describes the structure, the browser turns it into objects, and JavaScript manipulates those objects. The document object is the entry point to the whole tree.

The node tree

The DOM is hierarchical. The document is the root, <html> is its child, and everything else hangs beneath in a parent-child relationship. A node can have many children but only one parent.

document
└── html
    ├── head
    │   └── title
    └── body
        ├── h1
        └── p

Nodes come in several kinds. Element nodes represent tags. Text nodes hold the characters inside elements. Comment nodes represent comments. Most of the time you work with elements, but knowing text nodes exist explains whitespace surprises and why childNodes can include more than you expect.

Selecting elements

Before you can change anything, you have to find it. The modern workhorse is querySelector, which accepts any CSS selector.

// select.js
const title = document.querySelector("h1");
const firstCard = document.querySelector(".card");
const allLinks = document.querySelectorAll("a[target='_blank']");
const byId = document.getElementById("main");

allLinks.forEach((link) => link.classList.add("external"));

querySelector returns the first matching element or null. querySelectorAll returns a static NodeList of every match, which supports forEach but does not update when the DOM changes. The older methods getElementById, getElementsByClassName and getElementsByTagName still work; the last two return live collections that update automatically.

Reading and writing content

Once you have an element, you can read or replace its content.

// content.js
const heading = document.querySelector("#title");

heading.textContent;          // read the visible text
heading.textContent = "Hello"; // replace it, escaped as text

heading.innerHTML = "<em>Hi</em>"; // parses HTML

Use textContent whenever the value comes from a user, an API or anywhere you do not fully control. It treats everything as plain text and cannot inject markup. innerHTML parses a string as HTML, which is convenient for trusted templates but a serious cross-site scripting risk with untrusted input. If you must build markup from data, prefer creating elements and setting textContent on each.

Attributes, classes and styles

Attributes live on the element and can be read or changed directly.

// attrs.js
const link = document.querySelector("a");

link.getAttribute("href");
link.setAttribute("href", "/docs");
link.dataset.userId = "42"; // data-user-id="42"

link.classList.add("active");
link.classList.toggle("open");
link.classList.remove("hidden");

link.style.color = "tomato";

classList is the clean way to manage classes — it avoids string concatenation and accidental duplicate classes. The style property sets inline styles one property at a time; for anything thematic, prefer toggling a class and letting CSS decide.

Creating, inserting and removing nodes

You build new nodes with document.createElement and place them with the insertion methods.

// build.js
const item = document.createElement("li");
item.textContent = "New task";
item.classList.add("task");

const list = document.querySelector("#todos");
list.append(item);       // add at the end
list.prepend(item);      // add at the start
item.remove();           // detach from the tree

append accepts multiple nodes and even strings, while insertBefore gives you precise placement. When you need to insert many nodes, build them off-document and attach once:

// fragment.js
const fragment = document.createDocumentFragment();
for (const name of names) {
  const li = document.createElement("li");
  li.textContent = name;
  fragment.append(li);
}
list.append(fragment);

A DocumentFragment is a lightweight container that is not part of the page. Attaching it moves all of its children in a single operation, which triggers far less layout work than adding them one at a time.

Listening to events

Events are how your code responds to the user and the browser. Attach a listener with addEventListener.

// events.js
const button = document.querySelector("#save");

button.addEventListener("click", (event) => {
  event.preventDefault();
  console.log("clicked", event.target);
});

The event object carries useful information: target is the element that triggered the event, currentTarget is the element the listener is attached to, and preventDefault() cancels the browser’s default behaviour. Common events include click, input, change, submit, keydown, scroll and DOMContentLoaded.

Event delegation

Instead of attaching a listener to every child, attach one to a parent and inspect event.target. Because events bubble from the target up through its ancestors, the parent sees them all.

// delegate.js
const list = document.querySelector("#todos");

list.addEventListener("click", (event) => {
  const button = event.target.closest("button.delete");
  if (!button) return;
  button.closest("li").remove();
});

This is more efficient and, crucially, keeps working for elements added after the listener was attached. For dynamic lists and tables, delegation is the default pattern.

Traversing the tree

Sometimes you need to move from one node to its relatives.

// traverse.js
const item = document.querySelector(".item");

item.parentElement;        // nearest ancestor element
item.children;             // child elements only
item.firstElementChild;    // first child element
item.nextElementSibling;   // next sibling element
item.closest(".card");     // nearest ancestor matching a selector

The Element variants skip text and comment nodes, which is almost always what you want. closest is especially handy in event handlers, as the delegation example shows.

Performance and best practices

  • Batch your reads and writes. Reading layout properties like offsetHeight forces the browser to recalculate; interleaving reads and writes causes repeated reflows.
  • Build off-document. Use a DocumentFragment or an off-screen element, then attach once.
  • Cache your lookups. Store elements in variables instead of querying in a loop.
  • Prefer classes over inline styles. Toggle a class and keep presentation in CSS.
  • Use textContent by default. Only use innerHTML with trusted, sanitised content.
  • Delegate events for lists and anything dynamic.
  • Remove listeners you no longer need, especially on long-lived pages, to avoid leaks.

Common mistakes

  • Querying an element before it exists, then wondering why it is null.
  • Using innerHTML with user input and opening an XSS hole.
  • Attaching a listener to every row instead of delegating to the table.
  • Reading and writing layout properties in the same loop.
  • Mixing up event.target and event.currentTarget.
  • Assuming querySelectorAll is live when it is a static snapshot.

Where to go next

The DOM is the meeting point of everything you have learned. Combine it with ES6+ syntax for cleaner code, then feed it data with the Fetch API and keep it resilient with error handling. The fastest way to internalise it is to rebuild something small — a to-do list, a filterable gallery or a live search — entirely in vanilla JavaScript.

Setting text safely

textContent treats input as text. innerHTML parses it as markup, which is an XSS risk with untrusted data.

Prefer
const comment = "<b>hi</b>";
el.textContent = comment;
// renders the literal text
Avoid
const comment = "<img src=x onerror=alert(1)>";
el.innerHTML = comment;
// executes markup

Listening for clicks

Delegate to a parent so items added later still work and you attach one listener instead of many.

Prefer
list.addEventListener("click", (e) => {
  const li = e.target.closest("li");
  if (li) li.remove();
});
Avoid
document.querySelectorAll("li")
  .forEach((li) => {
    li.addEventListener("click", () => li.remove());
  });

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning DOM Manipulation?

Our interactive tutorial walks you through DOM Manipulation step by step — with quizzes and real code you can run in the browser.