Skip to main content

✏️ 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, and innerText
  • Read and write attributes with getAttribute/setAttribute and with direct properties
  • Explain the attribute vs. property distinction (and why value and checked surprise people)
  • Manipulate classes with the classList API and store custom data via dataset
  • 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:

graph TD A[Existing element] --> B[Content] A --> C[Attributes] A --> D[Classes] A --> E[Custom data] B --> B1[textContent] B --> B2[innerHTML] C --> C1[setAttribute] C --> C2[direct property] D --> D1[classList] E --> E1[dataset]
πŸ’‘ 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.

FeaturetextContentinnerHTMLinnerText
Parses HTMLNoYesNo
Includes hidden textYesYes (as HTML)No
Relative speedFastestSlowest (parses)Slower (reflow)
Safe with user inputYesNo β€” XSS riskYes

⚠️ 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.

Attribute versus property over time The HTML attribute stays at the initial value written in markup, while the DOM property updates to the current value as the user types. Attribute (HTML) the initial / default value getAttribute('value') "Initial" unchanged as user types Property (DOM) the current, live value input.value "What user typed" updates on every keystroke
Figure 1 β€” For form fields, the attribute holds the starting value while the property tracks what the user currently sees.
// <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

  1. As the user types in the textarea, update #count to show used / 150.
  2. When 20 or fewer characters remain, add a warn class to #count; remove it otherwise.
  3. Clicking #like toggles between β™‘ Like and β™₯ Liked, flips its data-liked and aria-pressed values, and toggles a liked class.
πŸ’‘ 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 style properties.
  • Use textContent for text; keep innerHTML for 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 className when you only meant to add or remove one class.
  • Assuming dataset values are numbers or booleans β€” they're always strings.
  • Setting individual style.* properties in a loop; batch with a class or cssText.

Summary & Quiz

πŸŽ‰ Key Takeaways

  • textContent is the fast, safe default; innerHTML parses markup (XSS risk); innerText reflects rendered, visible text.
  • Update attributes with setAttribute/removeAttribute or direct properties.
  • An attribute is the initial markup value; a property is the current live value β€” read the property for user state.
  • classList adds/removes/toggles classes cleanly; dataset stores custom data-* 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

πŸš€ 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.