βοΈ Modifying Element Content and Attributes
Most interface updates don't add or remove elements β they change ones already on the page. A price ticks up, an avatar swaps out, a checkbox flips. This lesson shows you how to update text, attributes, classes, and custom data precisely and safely.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Choose correctly between
textContent,innerHTML, andinnerText - Read and write attributes with
getAttribute/setAttributeand with direct properties - Explain the attribute vs. property distinction (and why
valueandcheckedsurprise people) - Manipulate classes with the
classListAPI and store custom data viadataset - Apply safe, performant patterns when updating the DOM in response to events or data
Estimated Time: 30β40 minutes β’ Difficulty: BeginnerβIntermediate
Hands-on: Build a live character counter and a "like" toggle that updates text, attributes, and classes.
In This Lesson
Updating Instead of Rebuilding
In the previous lesson you built elements from scratch. But once content is on the page, throwing it away and recreating it is wasteful and buggy β it discards event listeners, loses scroll position, and makes the screen flicker. The professional move is to reach into an existing element and change only what needs changing.
An element gives you four kinds of things you can modify, and it helps to keep them straight:
π‘ Analogy β renovating, not rebuilding. Changing content is redecorating a room; changing attributes is swapping a window or door; toggling classes is switching the whole room's theme. You renovate the one room that needs it β you don't demolish the house.
Modifying Content
Three properties change what's inside an element, and they behave differently enough that picking the wrong one is a real bug source.
textContent β plain text, fast and safe
const p = document.getElementById('status');
const current = p.textContent; // reads all text, including hidden
p.textContent = 'Saved!'; // replaces content; HTML is NOT parsed
p.textContent = '<b>hi</b>'; // shows the literal characters <b>hi</b>
Because it never parses HTML, textContent is the fastest option and is safe against XSS β the default choice for text.
innerHTML β parses markup
const box = document.getElementById('content');
box.innerHTML = '<h2>New heading</h2><p>With <em>emphasis</em>.</p>';
Powerful for injecting a chunk of structure at once, but it re-parses and rebuilds everything inside the element β and is dangerous with untrusted input.
innerText β visible text only
const header = document.getElementById('page-header');
const visible = header.innerText; // respects CSS: skips display:none text
innerText reflects what's actually rendered, so it ignores hidden elements and collapses whitespace. That awareness of layout means reading it can force the browser to compute styles (a reflow), making it slower than textContent.
| Feature | textContent | innerHTML | innerText |
|---|---|---|---|
| Parses HTML | No | Yes | No |
| Includes hidden text | Yes | Yes (as HTML) | No |
| Relative speed | Fastest | Slowest (parses) | Slower (reflow) |
| Safe with user input | Yes | No β XSS risk | Yes |
β οΈ The XSS trap
Never assign untrusted data to innerHTML:
element.innerHTML = userInput; // β a <script> or onerror can run
element.textContent = userInput; // β
shown as literal text
If you truly need to render user-provided HTML, sanitize it first with a vetted library such as DOMPurify.
Modifying Attributes
Attributes are the extra information on a tag β id, href, src, disabled, and so on. You have two ways to touch them.
getAttribute / setAttribute β works for anything
const link = document.getElementById('main-link');
link.getAttribute('href'); // read
link.setAttribute('href', '/new-page'); // write
link.setAttribute('target', '_blank');
link.hasAttribute('target'); // true
link.removeAttribute('target'); // delete
Direct property access β shorter, often faster
const img = document.getElementById('hero');
img.src = '/images/new-hero.jpg'; // same as setAttribute('src', ...)
img.alt = 'A new hero image';
img.width = 600;
img.hidden = true; // boolean attributes are just true/false
π‘ Which should I use?
For standard attributes, direct properties (img.src) are cleaner and slightly faster. Use setAttribute for non-standard or data-* attributes, or when you specifically want the raw HTML attribute value rather than the resolved property.
Attributes vs. Properties
This distinction trips up nearly everyone at first, so it's worth slowing down. The HTML attribute is the value written in the markup β think of it as the initial, default state. The DOM property is the live JavaScript value that reflects the current state, including anything the user has done since.
// <input id="name" value="Initial"> ...then the user types "Ray"
const input = document.getElementById('name');
input.value; // "Ray" (property = current state)
input.getAttribute('value'); // "Initial" (attribute = starting state)
// Checkboxes work the same way:
// <input type="checkbox" checked> ...then the user unticks it
checkbox.checked; // false (property = live state)
checkbox.getAttribute('checked'); // "" or "checked" (attribute = initial)
π Rule of thumb
To know what a form control currently holds, read the property (input.value, checkbox.checked). Reading the attribute gives you the page's original markup, not the user's input.
Classes & Data Attributes
The classList API
You can edit the className string directly, but classList gives you clean, targeted methods that don't clobber other classes:
const card = document.querySelector('.card');
card.classList.add('highlighted'); // add oneβ¦
card.classList.add('animate', 'fade-in'); // β¦or several
card.classList.remove('inactive');
card.classList.toggle('expanded'); // add if absent, remove if present
card.classList.toggle('selected', isOn); // force state with a boolean
card.classList.replace('loading', 'loaded');
card.classList.contains('editable'); // β true / false
β οΈ Don't overwrite classes by accident
card.className = 'active' throws away every other class the element had. Use classList.add('active') unless you truly mean to replace the entire class list.
Data attributes and dataset
Custom data-* attributes let you attach application data to an element without inventing non-standard attributes. The dataset property reads and writes them, converting kebab-case to camelCase:
// <div id="product" data-id="1234" data-in-stock="true"></div>
const product = document.getElementById('product');
product.dataset.id; // "1234"
product.dataset.inStock; // "true" (data-in-stock β inStock)
product.dataset.price = '499.99'; // adds data-price="499.99"
delete product.dataset.id; // removes data-id
π‘ Everything is a string
All dataset values are strings. data-in-stock="true" reads back as the string "true", which is truthy β so if (el.dataset.inStock) is always true. Compare explicitly: el.dataset.inStock === 'true', or parse numbers with Number(el.dataset.price).
Common Update Patterns
These small recipes combine the tools above into the updates you'll write constantly.
A live counter with a milestone highlight
const counterEl = document.getElementById('counter'); // cache the reference
let count = 0;
function increment() {
count++;
counterEl.textContent = count;
if (count % 10 === 0) {
counterEl.classList.add('milestone');
setTimeout(() => counterEl.classList.remove('milestone'), 1500);
}
}
Rendering fetched data (safely)
async function showWeather() {
const panel = document.getElementById('weather');
panel.classList.add('loading');
try {
const res = await fetch('/api/weather');
const data = await res.json();
// Build with textContent, not innerHTML, since values come from outside:
panel.querySelector('.city').textContent = data.city;
panel.querySelector('.temp').textContent = `${data.temp}Β°`;
panel.dataset.condition = data.condition; // drive CSS via a data attribute
} catch {
panel.querySelector('.city').textContent = 'Could not load weather';
} finally {
panel.classList.remove('loading');
}
}
Accessible inline validation
function validateEmail(input) {
const ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.value);
const msg = document.getElementById(input.id + '-msg');
input.classList.toggle('valid', ok);
input.classList.toggle('invalid', !ok);
input.setAttribute('aria-invalid', String(!ok)); // announce to screen readers
msg.textContent = ok ? 'Looks good!' : 'Enter a valid email address';
return ok;
}
Hands-on Exercise
ποΈ Character Counter & Like Toggle
Objective: Update content, attributes, and classes on existing elements in response to events.
Starter markup
<textarea id="bio" maxlength="150"></textarea>
<p id="count">0 / 150</p>
<button id="like" data-liked="false" aria-pressed="false">β‘ Like</button>
Your goal
- As the user types in the textarea, update
#countto showused / 150. - When 20 or fewer characters remain, add a
warnclass to#count; remove it otherwise. - Clicking
#liketoggles betweenβ‘ Likeandβ₯ Liked, flips itsdata-likedandaria-pressedvalues, and toggles alikedclass.
π‘ Hint
Listen for the input event on the textarea and read bio.value.length. For the button, derive the next state from like.dataset.liked === 'true', then set every piece from that boolean.
β Sample solution
const bio = document.getElementById('bio');
const count = document.getElementById('count');
const like = document.getElementById('like');
const MAX = 150;
bio.addEventListener('input', () => {
const used = bio.value.length;
count.textContent = `${used} / ${MAX}`;
count.classList.toggle('warn', MAX - used <= 20);
});
like.addEventListener('click', () => {
const liked = like.dataset.liked !== 'true'; // next state
like.dataset.liked = String(liked);
like.setAttribute('aria-pressed', String(liked));
like.classList.toggle('liked', liked);
like.textContent = liked ? 'β₯ Liked' : 'β‘ Like';
});
Best Practices
β Do
- Cache element references you reuse instead of re-querying them each call.
- Toggle a class rather than setting many inline
styleproperties. - Use
textContentfor text; keepinnerHTMLfor trusted, known markup only. - Read properties (
value,checked) for a control's current state. - Keep ARIA attributes in sync (
aria-pressed,aria-invalid) when state changes.
β οΈ Avoid
innerHTML +=β it destroys and rebuilds all existing child nodes and their listeners.- Overwriting
classNamewhen you only meant to add or remove one class. - Assuming
datasetvalues are numbers or booleans β they're always strings. - Setting individual
style.*properties in a loop; batch with a class orcssText.
Summary & Quiz
π Key Takeaways
textContentis the fast, safe default;innerHTMLparses markup (XSS risk);innerTextreflects rendered, visible text.- Update attributes with
setAttribute/removeAttributeor direct properties. - An attribute is the initial markup value; a property is the current live value β read the property for user state.
classListadds/removes/toggles classes cleanly;datasetstores customdata-*values (always strings).- Cache references, prefer classes over inline styles, and keep ARIA state in sync.
π― Quick Quiz
Question 1: A user types "Ray" into <input value="Guest">. What does input.getAttribute('value') return?
Question 2: An element has data-in-stock="true". Why is if (el.dataset.inStock) always truthy?
Question 3: You need to add one class to an element that already has several. Which is safest?
π Further Reading
- MDN β Element.innerHTML
- MDN β Element.classList
- MDN β Using data attributes
- DOMPurify β sanitizing HTML before innerHTML
π What's Next?
You've been toggling classes and setting the odd inline style. Next we go deeper into the visual layer β the style property, class-driven states, and CSS variables β in Dynamic Styling and Classes.