🎨 Modifying Elements and Attributes
Selecting an element is only half the story. The moment you hold a reference to a node, you can rewrite its text, restyle it, toggle its classes, and change its attributes — reshaping the page in front of the user's eyes. This lesson is where the DOM stops being read-only and starts responding to you.
Week 2 · Day 2 (Tuesday: DOM Manipulation) · Lecture 2
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Update an element's text with
textContentand know why it's safer thaninnerHTML - Explain the XSS risk of injecting untrusted HTML and choose the safe path
- Restyle elements with the
styleproperty and, preferably, withclassList - Add, remove, toggle, and test classes fluently
- Read and write attributes with
getAttribute/setAttributeand direct properties - Store and retrieve custom data with the
datasetAPI
Estimated Time: 60 minutes
Practice: Build a status-badge updater and a small theme-toggling helper.
In This Lesson
What You Can Change
Think of a selected element as an object with a handful of "dials" you can turn. Change its content and the words on screen update. Change its style or classes and it recolors, resizes, or hides. Change its attributes and links point elsewhere, inputs accept new values, images load new sources. Every dial is a plain JavaScript property or method.
innerHTML] B --> D[style] B --> E[classList] B --> F[attributes] B --> G[dataset] C --> H[Content updates] D --> I[Inline styling] E --> J[CSS-driven styling] F --> K[Behaviour changes] G --> L[Custom data]
The rest of this lesson walks each dial in turn, always favoring the safe, maintainable option over the quick-but-risky one.
Changing Text & HTML
There are three properties for reading and writing what's inside an element, and choosing the right one prevents both bugs and security holes.
| Property | What it sets | Parses HTML? | Sees hidden text? |
|---|---|---|---|
textContent | plain text | No — safe | Yes (all text) |
innerText | visible text | No | No (respects CSS) |
innerHTML | parsed HTML | Yes — risky | Yes (as markup) |
const el = document.querySelector('.content');
// textContent — sets/reads PLAIN TEXT. Fast and safe.
el.textContent = 'Simple text content';
// innerHTML — sets/reads HTML and PARSES it into real elements.
el.innerHTML = '<strong>Bold</strong> and <em>italic</em>';
// innerText — like textContent but respects styling (skips hidden text).
el.innerText = 'Only what the user can actually see';
// The three read differently on the same content:
el.innerHTML = '<p style="display:none">Hidden</p><p>Visible</p>';
console.log(el.textContent); // "HiddenVisible" — all text
console.log(el.innerText); // "Visible" — visible only
console.log(el.innerHTML); // the full markup string
💡 Default to textContent
Reach for textContent unless you specifically need to insert HTML tags. It's faster (no HTML parsing), it can't accidentally run scripts, and it does exactly what "set the text" implies.
The innerHTML Security Trap
The convenience of innerHTML hides a serious danger. If you drop untrusted input — anything a user typed or an API returned — into innerHTML, you may be handing an attacker the keys to your page. This is a cross-site scripting (XSS) vulnerability.
// ❌ DANGEROUS — this markup RUNS:
const userInput = '<img src="x" onerror="alert(\'hacked!\')">';
el.innerHTML = userInput; // the onerror handler fires immediately
// ✅ SAFE — the same string is shown as literal text, never executed:
el.textContent = userInput; // displays <img src="x" …> verbatim
innerHTML but harmless through textContent.⚠️ Rule: never put untrusted input into innerHTML
Use textContent for user-provided text. When you genuinely must render user HTML (a rich-text comment, say), sanitize it first with a vetted library such as DOMPurify — don't hand-roll your own escaping.
Changing Styles
The style property writes inline styles directly onto the element. CSS property names become camelCase (background-color → backgroundColor).
const box = document.querySelector('.box');
// One property at a time:
box.style.backgroundColor = 'royalblue';
box.style.color = 'white';
box.style.fontSize = '16px'; // note camelCase for font-size
box.style.borderRadius = '8px';
// Several at once with Object.assign:
Object.assign(box.style, {
padding: '15px',
transition: 'all 0.3s ease'
});
// CSS custom properties use setProperty (they keep their dashes):
box.style.setProperty('--brand', '#007bff');
// Remove an inline style by setting it to an empty string:
box.style.backgroundColor = '';
// Read the FINAL, computed value (after stylesheets apply):
const computed = getComputedStyle(box);
console.log(computed.fontSize); // e.g. "16px"
⚠️ Inline styles are a blunt instrument
Setting .style hard-codes values onto the element and overrides your stylesheet, which quickly becomes hard to maintain. For anything beyond a one-off tweak, define the look in CSS and toggle a class instead — that's the next section.
Working with classList
The classList API is the clean, modern way to drive styling from JavaScript: keep the actual CSS in your stylesheet, and just switch classes on and off.
const el = document.querySelector('.card');
// Add / remove (one or many):
el.classList.add('active');
el.classList.add('highlighted', 'featured');
el.classList.remove('featured');
// Toggle — add if absent, remove if present:
el.classList.toggle('collapsed');
// Toggle with a condition — force it on or off:
const shouldHighlight = true;
el.classList.toggle('highlighted', shouldHighlight);
// Test membership:
if (el.classList.contains('active')) {
console.log('the card is active');
}
// Swap one class for another in a single call:
el.classList.replace('old-theme', 'new-theme');
✅ classList over className
The older el.className = 'card active' replaces every class at once, wiping out anything already there. classList methods change just the one class you name, leaving the rest intact — far safer in real components.
Attributes & Properties
HTML attributes — href, src, type, disabled, and the rest — can be read and written two ways: the generic attribute methods, or direct property access.
const link = document.querySelector('a');
const input = document.querySelector('input');
// Generic attribute methods (work for ANY attribute):
link.getAttribute('href'); // read
link.setAttribute('href', 'https://example.com');
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener');
link.removeAttribute('target'); // remove
input.hasAttribute('required'); // test
// Direct property access (for standard attributes) is shorter:
input.value = 'New value';
input.type = 'password';
link.href = 'https://newsite.com';
// Boolean attributes are cleanest as properties:
input.disabled = true; // adds the attribute
input.disabled = false; // removes it
💡 Attribute vs property: a subtle difference
For a text input, the value attribute holds the original HTML default, while the value property holds what the user has currently typed. Reading input.value (the property) gives you the live value; getAttribute('value') gives you the initial one. For live form data, always use the property.
Data Attributes
Need to attach your own custom data to an element — a record id, a config flag, a state — without inventing invalid attributes? That's what data-* attributes are for, and the dataset property makes them effortless.
// HTML: <div class="user-card" data-user-id="12345" data-is-active="true">
const el = document.querySelector('.user-card');
// Read: data-user-id → dataset.userId (kebab-case → camelCase)
console.log(el.dataset.userId); // "12345"
console.log(el.dataset.isActive); // "true" (always a STRING)
// Write: dataset.lastLogin → data-last-login in the HTML
el.dataset.lastLogin = '2026-07-31';
el.dataset.userRole = 'admin';
// Everything comes back as a string — convert as needed:
const id = Number(el.dataset.userId); // 12345 (number)
const active = el.dataset.isActive === 'true'; // true (boolean)
// Handy for reading component config straight off an element:
function readConfig(element) {
return {
speed: parseInt(element.dataset.animationSpeed) || 300,
theme: element.dataset.theme || 'light',
autoplay: element.dataset.autoplay === 'true'
};
}
Output
el.dataset.userId → "12345" (string)
Number(el.dataset.userId) → 12345 (number)
el.dataset.isActive → "true" (string, not boolean!)
Practice & Quiz
🏋️ Exercise 1: A safe status updater
Goal: Write setStatus(selector, message, type) that updates an element's text safely and swaps its status class. It should do nothing (without crashing) if the element isn't found.
function setStatus(selector, message, type) {
// TODO: find the element; if missing, return false
// set its text safely; set class to `status status-${type}`
// return true on success
}
setStatus('.status', 'Saved!', 'success');
💡 Hint
Use querySelector and guard with if (!el) return false. Set the text with textContent (never innerHTML for a message), and set el.className or use classList.
✅ Solution
function setStatus(selector, message, type) {
const el = document.querySelector(selector);
if (!el) return false;
el.textContent = message; // safe — no HTML parsing
el.className = `status status-${type}`; // one clear status class set
return true;
}
🏋️ Exercise 2: Read a config off the DOM
Goal: Given <div class="widget" data-refresh="5000" data-open="true">, write getWidgetConfig() returning { refresh: 5000, open: true } with the correct types.
✅ Solution
function getWidgetConfig() {
const el = document.querySelector('.widget');
if (!el) return null;
return {
refresh: Number(el.dataset.refresh), // "5000" → 5000
open: el.dataset.open === 'true' // "true" → true
};
}
console.log(getWidgetConfig()); // { refresh: 5000, open: true }
🎯 Quick Quiz
Question 1: You need to display a message a user typed. Which property is the safe choice?
Question 2: What is the type of el.dataset.count when the HTML is data-count="7"?
Question 3: Which is the best way to switch an element's look between two states?
Best Practices & Pitfalls
✅ Do
- Default to
textContent; reserveinnerHTMLfor trusted, HTML-shaped content - Drive styling with
classListand keep the actual rules in CSS - Use
datasetfor custom data, and convert its string values to the type you need - Cache the element reference and reuse it instead of re-querying
- Check the element exists before touching its properties
❌ Don't
- Put untrusted user input into
innerHTML— that's the classic XSS hole - Reach for
el.className = '…'when you only mean to change one class - Treat
datasetvalues as numbers or booleans without converting - Scatter dozens of inline
.styleassignments where a single class would do
⚠️ Batch your changes to avoid layout thrash
// ❌ interleaving reads and writes forces repeated layout recalcs
box.style.width = '100px';
console.log(box.offsetWidth); // forces a reflow
box.style.height = '100px';
console.log(box.offsetHeight); // forces another
// ✅ prefer a class, or write all style changes together
box.classList.add('sized'); // one rule, one reflow
Summary
🎉 Key Takeaways
- Update text with
textContent; useinnerHTMLonly for trusted markup - Never inject untrusted input into
innerHTML— that's the XSS trap - Prefer
classList(with CSS rules) over piles of inline.styleassignments classListmethods change one class;className =replaces them all- Manage attributes with
getAttribute/setAttributeor direct properties datasetstores customdata-*values — always as strings, so convert them
📚 Additional Resources
🚀 What's Next?
You can now change any element that already exists. Next you'll conjure brand-new ones out of thin air and tear old ones down: Creating and Removing Elements — createElement, insertion methods, document fragments, and clean removal.
🎉 Great progress!
Content, style, classes, attributes, data — the page now bends to your code. Next, you'll build parts of it from scratch.