JavaScript DOM Basics

The DOM is the bridge between JavaScript and HTML. Learn to select, traverse, create, and inspect elements using the modern DOM API - the foundation of all browser-side JavaScript.

Beginner 10 min read 10 examples

What is the DOM?

The Document Object Model (DOM) is the browser's in-memory representation of an HTML page as a tree of nodes. JavaScript interacts with the page by reading and modifying this tree.

HTML Page structure
<!-- This HTML... -->
<html>
  <body>
    <h1 id="title">Hello</h1>
    <ul class="list">
      <li>Item 1</li>
      <li>Item 2</li>
    </ul>
  </body>
</html>

<!-- ...becomes a tree of nodes:
document
  html
    body
      h1 #title
        "Hello" (text node)
      ul .list
        li
          "Item 1" (text node)
        li
          "Item 2" (text node)
-->
JavaScript
// document is the entry point to the DOM
console.log(document.title);   // "My Page"
console.log(document.URL);     // current URL
console.log(document.body);    // <body> element
console.log(document.head);    // <head> element
console.log(document.documentElement); // <html> element

Selecting Elements

querySelector and querySelectorAll accept any CSS selector and are the modern standard for element selection.

JavaScript
// querySelector - returns FIRST matching element (or null)
const title   = document.querySelector("#title");        // by id
const btn     = document.querySelector(".btn");          // by class
const input   = document.querySelector("input[type='email']"); // by attribute
const first   = document.querySelector("ul > li");       // CSS selector
const heading = document.querySelector("h1, h2, h3");   // first of any heading

// querySelectorAll - returns ALL matching elements (NodeList)
const items   = document.querySelectorAll(".list li");
const buttons = document.querySelectorAll("button");

// Convert NodeList to Array to use array methods
const itemsArr = Array.from(items);
const texts    = [...items].map(el => el.textContent);

// Legacy selectors (still useful)
const byId    = document.getElementById("title");    // returns Element or null
const byClass = document.getElementsByClassName("btn"); // live HTMLCollection
const byTag   = document.getElementsByTagName("p");     // live HTMLCollection

// Scoped selection: search within a specific element
const nav  = document.querySelector("nav");
const link = nav.querySelector("a.active"); // search inside nav only
Always Check for null

querySelector returns null if no match is found. Calling a method on null throws a TypeError. Always check: const el = document.querySelector("#id"); if (el) { el.textContent = "..."; } or use optional chaining: el?.textContent.

DOM Traversal

Navigate the DOM tree using parent, child, and sibling relationships:

JavaScript
const list = document.querySelector("ul");

// Children (Element-only, ignores text nodes)
console.log(list.children);         // HTMLCollection of li elements
console.log(list.firstElementChild); // first li
console.log(list.lastElementChild);  // last li
console.log(list.children.length);   // number of child elements

// Parent
const item = document.querySelector("li");
console.log(item.parentElement);     // the ul
console.log(item.parentElement.parentElement); // further up

// Siblings
const firstItem = list.firstElementChild;
console.log(firstItem.nextElementSibling);     // next li
console.log(firstItem.previousElementSibling); // null (it is the first)

// closest() - find nearest ancestor matching selector (including self)
const btn = document.querySelector("button");
const form = btn.closest("form"); // walks up until finding a form
const card = btn.closest(".card"); // nearest .card ancestor

// matches() - test if element matches a selector
console.log(item.matches("li.active")); // true/false
console.log(item.matches(":first-child")); // true/false

Creating Elements

JavaScript
// Create element
const newItem = document.createElement("li");
newItem.textContent = "New Item";
newItem.className   = "list-item";

// Add to DOM
const list = document.querySelector("ul");
list.appendChild(newItem);           // add at end
list.prepend(newItem);               // add at start
list.insertBefore(newItem, firstLi); // before specific element

// Modern: insertAdjacentElement
// "beforebegin" | "afterbegin" | "beforeend" | "afterend"
list.insertAdjacentElement("afterend", newItem); // after the list

// Create multiple elements efficiently
const fragment = document.createDocumentFragment();
["Apple", "Banana", "Cherry"].forEach(name => {
    const li = document.createElement("li");
    li.textContent = name;
    fragment.appendChild(li); // append to fragment (no reflow yet)
});
list.appendChild(fragment); // one DOM update

// innerHTML for simple trusted HTML insertion
list.innerHTML += "<li>New Item</li>"; // simple but re-parses all children
list.insertAdjacentHTML("beforeend", "<li>New Item</li>"); // more efficient

textContent vs innerHTML

JavaScript
const el = document.querySelector("#output");

// textContent - plain text, no HTML parsing
el.textContent = "Hello <b>World</b>";
// Renders literally: Hello <b>World</b> (the tags are text)

// Safe for user input (prevents XSS!)
const userInput = '<script>alert("xss")</script>';
el.textContent = userInput; // safe - displays as text

// innerHTML - parses HTML markup
el.innerHTML = "Hello <b>World</b>";
// Renders: Hello World (bold)

// DANGER: never use innerHTML with user input
// el.innerHTML = userInput; // XSS vulnerability!

// innerText vs textContent
// textContent: raw text including hidden elements, no layout considered
// innerText: visible text only, considers CSS (getComputedStyle) - slower
el.innerText = "Hello"; // respects CSS, triggers layout

// outerHTML - includes the element itself
console.log(el.outerHTML); // <div id="output">Hello</div>

// Remove all children
el.textContent = ""; // fastest way to clear content
// el.innerHTML = ""; // also works but slower

Attributes and Classes

JavaScript
const btn = document.querySelector("button");

// HTML attributes
btn.setAttribute("disabled", "");    // add attribute
btn.getAttribute("type");            // read: "button"
btn.removeAttribute("disabled");     // remove
btn.hasAttribute("disabled");        // false

// Properties (faster than getAttribute for standard attributes)
btn.disabled = true;    // same as setAttribute("disabled", "")
btn.id       = "submit-btn";
btn.type     = "submit";

// Dataset - custom data-* attributes
// <div data-user-id="42" data-role="admin">
const div = document.querySelector("[data-user-id]");
console.log(div.dataset.userId); // "42" (camelCase access)
console.log(div.dataset.role);   // "admin"
div.dataset.newProp = "value";   // adds data-new-prop="value"

// classList - manage CSS classes
const card = document.querySelector(".card");
card.classList.add("active", "highlight");    // add multiple
card.classList.remove("hidden");              // remove
card.classList.toggle("open");               // add if absent, remove if present
card.classList.toggle("dark", isDark);       // add/remove based on condition
card.classList.contains("active");           // true/false
card.classList.replace("old-class", "new-class"); // swap
console.log(card.className); // all classes as string

Inline Styles

JavaScript
const box = document.querySelector(".box");

// Set inline styles (camelCase property names)
box.style.backgroundColor = "blue";   // background-color
box.style.fontSize         = "18px";  // font-size
box.style.marginTop        = "20px";  // margin-top
box.style.transform        = "rotate(45deg)";

// Remove inline style
box.style.backgroundColor = ""; // set to empty string removes it

// Read computed style (includes CSS from stylesheets)
const styles = getComputedStyle(box);
console.log(styles.width);           // "300px" (actual computed value)
console.log(styles.backgroundColor); // "rgb(0, 0, 255)"

// Better approach: toggle classes instead of direct style manipulation
// Keeps styling in CSS, JS only manages state
box.classList.add("box-large");    // define .box-large in CSS
box.classList.remove("box-small");

Frequently Asked Questions

getElementById("id") selects one element by its id - it is fast but limited to ids. querySelector("#id") does the same but accepts any CSS selector, making it far more flexible: querySelector(".btn.active"), querySelector("input[type=email]"), querySelector("ul > li:first-child"). Prefer querySelector/querySelectorAll for consistency and power. getElementById is still marginally faster for id lookups.

textContent sets or gets the plain text content of an element - HTML tags are treated as literal text, not parsed. Use it for displaying user-provided content since it prevents XSS. innerHTML sets or gets HTML markup - the browser parses it and creates real elements. Use it only with trusted content you control. Never put user input into innerHTML.

querySelectorAll returns a NodeList, not an array. NodeLists are array-like but lack most array methods (no map, filter, etc.). You can convert to an array with Array.from(nodeList) or [...nodeList]. NodeLists returned by querySelectorAll are static (do not update when the DOM changes), unlike NodeLists from childNodes which are live.

Use createElement when you need to attach event listeners to elements before insertion, when building complex structures programmatically, or when performance matters for many insertions. Use template literals with innerHTML for simpler cases where you are inserting trusted, static HTML. Never use innerHTML with user-provided content - it enables XSS attacks.