Free lesson · JavaScript Basic to Advance
DOM Fundamentals: Selecting and Reading
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:
- Use
textContentfor reading and writing plain text. It's fast and doesn't parse HTML. - Use
innerHTMLonly when you specifically need to read or set HTML markup. - Avoid
innerTextunless you specifically need the rendered-text behavior.
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
innerHTMLwith untrusted content. This is the classic XSS vulnerability. SettinginnerHTMLparses the string as HTML, so any<script>tags or event handler attributes in the input get executed.Confusing live and static collections.
getElementsByClassNamereturns a live collection that updates as the DOM changes.querySelectorAllreturns 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
datasetvalues are strings.data-count="5"becomes the string"5", not the number5. 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:
- Select an element and inspect its
parentNode,children, and siblings- Read
textContentandinnerHTMLof the same element — observe the difference- Use
getElementsByClassNameandquerySelectorAllto select the same elements- Modify the DOM by adding a class to an element
- 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.