AlgoInsight

Free lesson · JavaScript Basic to Advance

DOM Fundamentals: Selecting and Reading

Easy5 min read

What the DOM Is

The DOM (Document Object Model) is the browser's representation of an HTML document as a tree of JavaScript objects. Each HTML element becomes a node in the tree, and JavaScript can read and modify those nodes through specific APIs.

<!-- HTML source -->
<div id="container">
  <h1 class="title">Hello</h1>
  <p>A paragraph</p>
</div>
// JavaScript view of the same structure
const container = document.getElementById("container");
const title = container.querySelector(".title");
const paragraph = container.querySelector("p");

console.log(title.textContent);   // "Hello"
console.log(container.children);   // [h1.title, p]

The HTML you write becomes a tree of objects. The objects have properties (like textContent, id, children) and methods (like querySelector, addEventListener). All DOM manipulation goes through this object interface.

This Topic covers selecting elements and reading their state. Topic 12.3 covers modifying the DOM.

Interactive animation: Every div rides this chain - an element is an object whose properties and methods, like querySelector, come from its prototypes.

The Document Object

The entry point is document — a global object representing the entire HTML document. Almost every DOM operation starts here.

document.documentElement   // the <html> element
document.head              // the <head> element
document.body              // the <body> element
document.title             // the page title (read or write)
document.URL               // the current URL (read-only)
document.cookie            // cookies for the current document
document.activeElement     // currently focused element

These properties are starting points. The interesting work happens through methods that find specific elements.

Selecting Elements

Several methods for selecting elements, each suited to different cases:

document.getElementById(id) — fastest, returns one element by its id attribute, or null if not found.

const header = document.getElementById("main-header");

document.querySelector(cssSelector) — most flexible, returns the first element matching a CSS selector, or null.

const firstButton = document.querySelector("button");
const submitButton = document.querySelector("button.submit");
const formField = document.querySelector("#email-form input[type=email]");

document.querySelectorAll(cssSelector) — returns all matching elements as a NodeList.

const allButtons = document.querySelectorAll("button");
const allLinks = document.querySelectorAll("a[href^='https']");

allButtons.forEach(btn => console.log(btn.textContent));

document.getElementsByClassName(className) — returns a live HTMLCollection of elements with the given class.

document.getElementsByTagName(tagName) — returns a live HTMLCollection of elements with the given tag name.

The newer querySelector / querySelectorAll methods accept full CSS selector syntax and are the modern preferred approach. The older getElementById, getElementsByClassName, and getElementsByTagName are still slightly faster for their specific cases but less flexible.

graph TD
    A[Selection methods] --> B[getElementById<br/>fastest, returns one]
    A --> C[querySelector<br/>flexible, returns first match]
    A --> D[querySelectorAll<br/>flexible, returns all matches]
    A --> E[getElementsByClassName<br/>live, returns collection]
    A --> F[getElementsByTagName<br/>live, returns collection]

    style C fill:#10b981,color:#fff
    style D fill:#10b981,color:#fff
This diagram shows the main DOM selection methods. getElementById is the fastest and returns a single element matching the given id. querySelector is the most flexible — it accepts any CSS selector and returns the first matching element. querySelectorAll accepts the same selectors but returns all matches as a NodeList. The older getElementsByClassName and getElementsByTagName return live HTMLCollections that auto-update if the DOM changes, which can be either convenient or confusing depending on context.

Interactive animation: One method, every selector you know - querySelector takes any CSS selector and returns null when nothing matches.

Static vs Live Collections

A subtle but important distinction: querySelectorAll returns a static NodeList — a snapshot taken at the moment of the call. getElementsByClassName and getElementsByTagName return live HTMLCollections that automatically update if matching elements are added or removed from the DOM.

const liveList = document.getElementsByClassName("item");
const staticList = document.querySelectorAll(".item");

console.log(liveList.length, staticList.length);   // both, say, 3

// Add a new element with class="item"
const newItem = document.createElement("div");
newItem.className = "item";
document.body.appendChild(newItem);

console.log(liveList.length, staticList.length);   // 4, 3
// liveList updated; staticList is a snapshot

The live behavior of getElementsByClassName can be useful but is often surprising. Modern code mostly uses querySelectorAll for predictable static behavior. Use the live versions deliberately if you specifically want to react to DOM changes.

Interactive animation: The same question, two behaviours - the querySelectorAll snapshot stays put while the live collection counts the new element.

Element Properties

Once you have an element, you can read its state through properties:

const el = document.querySelector("#example");

// Content
el.textContent       // all text content, including descendants
el.innerHTML         // the HTML markup inside (returns string)
el.innerText         // similar to textContent but considers rendering

// Attributes
el.id                // the id attribute
el.className         // the class attribute (string)
el.classList         // a DOMTokenList for class manipulation
el.dataset           // an object exposing data-* attributes

// Structure
el.parentNode        // the parent element
el.children          // child elements as an HTMLCollection
el.childNodes        // includes text and comment nodes (less commonly useful)
el.firstElementChild // first child element
el.lastElementChild  // last child element
el.nextElementSibling
el.previousElementSibling

// Box and position
el.offsetWidth       // rendered width including padding and border
el.offsetHeight
el.offsetTop         // position relative to offsetParent
el.offsetLeft
el.getBoundingClientRect()   // detailed positioning info

// State
el.tagName           // tag name in uppercase, e.g. "DIV"
el.matches(selector) // boolean — does this element match a CSS selector?

These are the most-used properties. There are many more — the DOM spec is large — but these cover almost all everyday work.

textContent vs innerHTML vs innerText

A common confusion. Three similar-sounding properties with different behaviors:

textContent — returns or sets all text content, including text in descendant elements. Ignores HTML markup (treats it as text). Doesn't consider rendering.

innerHTML — returns or sets HTML markup. Reading gives you a string with HTML tags. Setting parses the string as HTML and replaces the content.

innerText — similar to textContent but considers rendering. If text is hidden by CSS, innerText won't include it. Slower than textContent because it triggers layout.

// Given this element: <div>Hello <span style="display:none">hidden</span> world</div>
el.textContent   // "Hello hidden world"   — includes hidden text
el.innerHTML     // 'Hello <span style="display:none">hidden</span> world'
el.innerText     // "Hello world"   — excludes hidden text

// Setting
el.textContent = "<b>bold</b>";   // displays literally as "<b>bold</b>"
el.innerHTML = "<b>bold</b>";     // displays as actual bold text

The rule of thumb:

innerHTML has a security risk: setting it with untrusted content allows HTML injection (cross-site scripting). For untrusted text, always use textContent.

Attributes vs Properties

A subtle DOM concept: elements have both attributes (from HTML markup) and properties (on the JavaScript object). They're related but not always identical.

<input id="email" type="email" value="default@example.com">
const input = document.getElementById("email");

input.value                       // current value, reflects user typing
input.getAttribute("value")       // "default@example.com" — original HTML value

input.value = "new@example.com";
input.getAttribute("value")       // STILL "default@example.com"

For most elements and most attributes, the property and the attribute mirror each other. But for some — especially form fields — the property reflects current state while the attribute reflects the original HTML.

The general rule: use properties for current state (input.value, checkbox.checked), use getAttribute/setAttribute for the literal HTML attribute value.

Interactive animation: input.value, after the user types - the property follows the typing while getAttribute still returns the HTML default.

Working With classList

The classList property exposes a DOMTokenList — an API for adding, removing, and checking classes without manually manipulating the className string.

const el = document.querySelector(".card");

el.classList.add("highlighted");
el.classList.remove("disabled");
el.classList.toggle("active");
el.classList.toggle("dark-mode", true);   // force add
el.classList.toggle("dark-mode", false);  // force remove

el.classList.contains("highlighted");     // true
el.classList.replace("old", "new");

// Iterate over all classes
for (const className of el.classList) {
  console.log(className);
}

classList is cleaner than the older approach of splitting className by spaces and rejoining. Use it for any class manipulation.

Working With dataset

HTML data-* attributes are exposed via the dataset property as a plain JavaScript object.

<div id="product" data-product-id="42" data-product-name="Widget" data-in-stock="true">
  ...
</div>
const product = document.getElementById("product");

console.log(product.dataset.productId);    // "42"
console.log(product.dataset.productName);  // "Widget"
console.log(product.dataset.inStock);      // "true"

// Note: values are always strings; convert as needed
const isInStock = product.dataset.inStock === "true";
const productId = parseInt(product.dataset.productId, 10);

// Set new data attributes
product.dataset.lastViewed = Date.now();

The kebab-case in HTML (data-product-id) becomes camelCase in JavaScript (productId). All values are strings — explicit conversion is needed for other types.

dataset is the modern way to attach custom data to DOM elements. Before it existed, developers used arbitrary attributes or JavaScript closures. dataset keeps the data with the element where it belongs.

Common Pitfalls

Warning: The DOM selection traps.

Using innerHTML with untrusted content. This is the classic XSS vulnerability. Setting innerHTML parses the string as HTML, so any <script> tags or event handler attributes in the input get executed.

Confusing live and static collections. getElementsByClassName returns a live collection that updates as the DOM changes. querySelectorAll returns a static snapshot. Iterating a live collection while mutating the DOM can produce surprising results.

Treating attributes and properties as the same. Most of the time they match. For form fields, they often don't — the attribute is the original HTML, the property is the current state.

Forgetting that dataset values are strings. data-count="5" becomes the string "5", not the number 5. Explicit conversion is required for numeric or boolean values.

Why This Matters in Interviews

Interview: DOM API fluency is foundational for frontend interviews. Knowing the four main selection methods, understanding the textContent/innerHTML/innerText distinction, and being able to use classList and dataset confidently signals real frontend experience.

Topic 12.7 — Interview Lens: Browser and DOM Questions — covers the deeper interview-prep treatment.

Try It

Exercise: In a browser developer console on any page, try:

  1. Select an element and inspect its parentNode, children, and siblings
  2. Read textContent and innerHTML of the same element — observe the difference
  3. Use getElementsByClassName and querySelectorAll to select the same elements
  4. Modify the DOM by adding a class to an element
  5. Check whether the two collection types reacted differently to the change

By the time you've done these inspections, the selection APIs are familiar enough to use in real code.

Back to JavaScript Basic to Advance