Historical Context & Motivation
In the earliest days of the web, pages were completely static. A server sent an HTML file to your browser, the browser displayed it, and that was that — no interactivity, no dynamic updates. If you wanted to change anything on the page, you had to reload the entire thing from the server. This was fine when the web was mostly academic documents, but as the internet grew into a platform for applications and commerce, developers needed a way to manipulate page content on the fly.
The answer was the Document Object Model (DOM) — a programming interface that lets JavaScript reach into an HTML page and read or change any part of it while the user is still looking at it. Understanding how the DOM evolved helps you appreciate why reading and updating text, attributes, and classes is so central to modern web development.
getElementById() give developers a standard way to locate elements across all browsers.querySelector() and querySelectorAll(), letting developers select elements with CSS-style selectors — far more flexible than older methods.The core question this lesson addresses is straightforward: once an HTML page is loaded and displayed, how does JavaScript read existing content and then change it in real time? Specifically, how do you work with an element's text, its HTML attributes (like src or href), and its CSS classes?
Core Principles & Definitions
Before you start writing code, you need a mental model for how the browser organizes a web page. Every HTML document is parsed into a tree of objects called nodes. Each HTML tag becomes an element node, the text inside a tag becomes a text node, and the tag's attributes (like id or class) are stored as properties on that element object. JavaScript can both read and write these properties, which instantly updates what the user sees on screen.
Text Content
textContent to read or set the plain text inside an element. Use innerHTML when you need to include HTML tags in the content.Attributes
getAttribute() and setAttribute() to read or change any HTML attribute, such as src, href, or data-* custom attributes.CSS Classes
classList object with methods like add(), remove(), toggle(), and contains() for managing CSS classes cleanly.Selection First
document.querySelector() (returns one element) and document.querySelectorAll() (returns a list).Visual Explanation — The DOM Tree
The diagram below shows how a small HTML snippet is represented as a DOM tree. Each rounded box is a node. Element nodes are colored, and the text inside them appears as child text nodes. Notice how attributes like id, class, and href are attached to their parent element nodes. JavaScript accesses any of these through the DOM API.
<div>. The cyan node is an <h1> and the pink node is an <a> link. The lower boxes show how JavaScript reads values on the left and updates them on the right.Notice the pattern: you always start by selecting an element, then you either read a property to get its current value, or you assign a new value to change what the user sees. This select → read/write pattern is the foundation of every DOM interaction you will ever write.
How It Works — The API in Detail
Reading & Writing Text
Every element node has two properties for its text. The textContent property returns or sets the plain text inside the element, stripping away any nested HTML tags. In contrast, innerHTML returns or sets the full HTML markup inside the element, which means it can include tags like <strong> or <em>. Use textContent when you only need plain text — it is faster and safer because it does not parse HTML.
element is a reference to a DOM node obtained via querySelector() or similar. Reading returns a string; assigning replaces all child content.innerHTML with user-supplied data can introduce security vulnerabilities (cross-site scripting). Prefer textContent unless you truly need HTML.Reading & Writing Attributes
HTML attributes like src, href, alt, and custom data-* attributes are accessed through three main approaches. You can use getAttribute(name) and setAttribute(name, value) for any attribute. Many common attributes also have direct property access — for example, img.src or a.href. You can remove an attribute entirely with removeAttribute(name).
getAttribute() always returns a string (or null if the attribute does not exist). setAttribute() always takes two string arguments: the name and the new value.Reading & Writing Classes
CSS classes control how elements look, and JavaScript gives you the classList object to manage them cleanly. Instead of clumsily rewriting the entire class attribute string, you can call individual methods on classList. The four methods you will use most often are add(), remove(), toggle(), and contains().
add() and remove() at once, for example: el.classList.add('btn', 'primary').Detailed Breakdown — Choosing the Right Tool
With multiple ways to read and change elements, it helps to see them side by side. The table below organizes every method by what it does (read or write) and what part of the element it targets (text, attributes, or classes). The diagram that follows visualizes the decision-making process.
| Task | Property / Method | Returns | Notes |
|---|---|---|---|
| Read plain text | el.textContent | String | Strips HTML tags; fastest option |
| Write plain text | el.textContent = '...' | — | Replaces all children with text |
| Read HTML | el.innerHTML | String (HTML) | Includes nested tags |
| Write HTML | el.innerHTML = '...' | — | Parses HTML; avoid with user input |
| Read attribute | el.getAttribute('name') | String or null | Works for any attribute |
| Write attribute | el.setAttribute('name', 'val') | — | Creates the attribute if missing |
| Remove attribute | el.removeAttribute('name') | — | Removes it from the element entirely |
| Add class | el.classList.add('x') | — | No effect if class already present |
| Remove class | el.classList.remove('x') | — | No error if class is absent |
| Toggle class | el.classList.toggle('x') | Boolean | Returns true if class was added |
| Check class | el.classList.contains('x') | Boolean | Useful in conditionals |
getAttribute() and direct property access.Worked Example — Building a Profile Card Editor
Imagine you have a simple profile card on a web page. The HTML looks like this:
card and light. The image points to old-avatar.png. We want to update the name, bio, image, and toggle dark mode.querySelector() to grab references to each element we need to modify. We store them in variables so we can reuse them.
const card = document.querySelector('#card');
const img = card.querySelector('img');
const name = card.querySelector('h2');
const bio = card.querySelector('.bio');card, img, name, biotextContent on the <h2> element.
name.textContent = 'Alex Rivera';innerHTML instead of textContent.
bio.innerHTML = '<strong>Full-stack</strong> engineer from Austin.';src and alt attributes on the <img> element.
img.setAttribute('src', 'new-avatar.png');
img.setAttribute('alt', 'Photo of Alex Rivera');light class and add a dark class to the card. Alternatively, we can toggle them.
card.classList.remove('light');
card.classList.add('dark');"card dark". If the CSS defines different colors for .dark, the card's appearance changes instantly.console.log(name.textContent); // 'Alex Rivera'
console.log(img.getAttribute('src')); // 'new-avatar.png'
console.log(card.classList.contains('dark')); // trueStrengths, Pitfalls, and Best Practices
Now that you know the methods, let's discuss when each shines and where things can go wrong. Every tool in the DOM API has trade-offs, and picking the right one will save you from subtle bugs and security issues.
| Approach | Strengths | Pitfalls |
|---|---|---|
textContent | Fast, safe from XSS attacks, simple to use for plain text updates. | Cannot insert HTML — all tags are treated as literal text characters. |
innerHTML | Lets you inject formatted HTML, create new child elements on the fly. | Vulnerable to cross-site scripting (XSS) if you insert user-supplied strings. Slower because the browser re-parses HTML. |
getAttribute / setAttribute | Works for any attribute, including custom data-* attributes. Universal and predictable. | Returns the raw HTML string, which may differ from the resolved property (e.g., relative vs. absolute URLs). |
Direct property access (el.src) | Shorter syntax, returns the computed/resolved value (full URL, boolean for disabled, etc.). | Not available for all attributes — custom or non-standard attributes require getAttribute. |
classList | Clean API with add, remove, toggle, contains. Does not accidentally overwrite other classes. | Only manages class names — does not directly set inline styles. Use el.style for that. |
className (the older way) | Simple string replacement; sometimes useful when you want to set all classes at once. | Replaces the entire class list — easy to accidentally remove classes you wanted to keep. |
textContent for text and classList for classes. Only reach for innerHTML when you genuinely need HTML, and never put unsanitized user input into it. Think of it like a microwave versus an oven: the microwave (textContent) is quick and safe for everyday tasks, while the oven (innerHTML) is powerful but you need to be careful not to burn yourself.Connection to Frameworks & Advanced Concepts
The methods you have learned are sometimes called "vanilla DOM" manipulation — meaning they use the browser's built-in API with no extra libraries. Modern frameworks like React, Vue, and Angular abstract these operations behind higher-level patterns, but they still perform the same underlying reads and writes. Understanding vanilla DOM makes you a stronger developer because you can debug framework behavior, build lightweight features without adding dependencies, and perform well in technical interviews.
| Concept | Vanilla DOM (This Lesson) | React / Vue Equivalent |
|---|---|---|
| Updating text | el.textContent = 'Hello' | Change state variable → framework re-renders automatically |
| Updating attributes | el.setAttribute('src', url) | Bind attribute to state: <img :src="url"> (Vue) |
| Toggling classes | el.classList.toggle('active') | Conditional class: className={active ? 'active' : ''} (React) |
| Selection | document.querySelector() | useRef() hook (React) or $refs (Vue) |
| Batch updates | Manual — each change triggers a repaint | Virtual DOM diffs changes and batches DOM writes for efficiency |
As you advance, you will encounter concepts like the Virtual DOM (a lightweight copy of the real DOM that frameworks use to minimize expensive updates), data binding (automatically syncing a variable's value with what appears on screen), and Shadow DOM (an encapsulated DOM subtree used in Web Components). All of these build on the same reading and writing primitives you practiced today.
document.createElement(), appendChild(), and remove(). Then you will learn about event listeners so that your updates happen in response to user actions like clicks and keystrokes.Practice Problems
Test your understanding with these five problems. They start conceptual and build toward more challenging scenarios. For each one, think through your answer before checking.
textContent and innerHTML. When would you choose one over the other?<p id="msg">Welcome!</p>, write JavaScript to (a) read the current text, (b) change it to "Goodbye!", and (c) add a class called highlight.<img id="photo" src="cat.jpg" alt="A cat"> tag. Write code that checks whether the alt attribute contains the word "cat". If it does, change the src to "dog.jpg" and the alt to "A dog".<li class="task">Buy groceries</li>. When a user completes a task, you want to add a done class and prepend the text with a ✓ symbol. Write a function completeTask(li) that takes a list item element and makes both changes, but only if the task has not already been marked done.document.body.className = 'dark'; It works — the page goes dark. But later they discover that their navigation menu has lost its styling. Explain what went wrong and write a corrected version of the code.Lesson Summary
In this lesson you learned how JavaScript interacts with the Document Object Model (DOM) to read and update web page content in real time. You explored three core areas: text (using textContent for plain text and innerHTML for HTML markup), attributes (using getAttribute(), setAttribute(), and direct property access), and CSS classes (using classList.add(), remove(), toggle(), and contains()).
The universal pattern is select → read or write: use querySelector() to grab an element, then access its properties or call its methods. Remember to prefer textContent over innerHTML for safety, and always use classList instead of overwriting className to avoid accidentally removing other classes. These vanilla DOM skills are the foundation of every interactive web application — including those built with frameworks like React and Vue.