DOM and events · 3 / 10
lesson 3

Reading and changing the DOM

textContent vs innerHTML vs innerText, attributes vs properties, and the dataset shortcut.

~ 15 min read·lesson 3 of 10
0 / 10

You have a reference to an element. Now you want to read what is in it or change what is in it. This is where most DOM bugs come from — there are several ways to read text, several ways to write it, and the differences matter for both correctness and security.

The good news is that the right defaults are simple. The not-so-good news is that the wrong defaults are everywhere on the internet, and they will bite you.

Reading and writing text

The cleanest property for both reading and writing is textContent.

text.js
const heading = document.querySelector('h1');

console.log(heading.textContent); // "Hello"

heading.textContent = 'Goodbye';
// the heading now reads "Goodbye"

When you read textContent, you get the text inside the element with all the tags stripped out. When you write to it, you replace whatever was inside with that exact string — and the string is treated as text, not as markup. The string <b>hi</b> becomes the literal characters <b>hi</b> on the page, not a bold "hi".

That last point is the security one. If the string came from a user — a comment, a search query, a username — and you put it into the DOM with textContent, no markup hidden inside the string can take effect. The browser sees text, displays text. Done.

You will also see two cousins of textContent: innerText and nodeValue. They look similar and trip people up.

  • innerText cares about CSS — it returns what the element looks like rendered, so hidden text is omitted and whitespace is collapsed. Reading it forces the browser to do layout work, which is slow on big pages.
  • nodeValue only makes sense on text nodes, not elements. You probably will not need it.

The practical rule: use textContent for both reading and writing text unless you have a specific reason to do otherwise.

Tip

Setting textContent = '' is the fastest way to empty an element. The whole subtree goes away in one assignment.

check your understanding
You want to display a username that came from a server response: name = '<script>alert(1)</script>'. You write el.textContent = name. What happens?

innerHTML and what it really does

innerHTML is the property you should know about, and use carefully. Setting it parses the assigned string as HTML and rebuilds the element's contents from the parsed tree.

innerhtml.js
const card = document.querySelector('.card');
card.innerHTML = '<h2>Title</h2><p>Body text.</p>';
// the card now contains a real h2 and a real p

That is genuinely useful. You can build a chunk of UI as a string template and stamp it into the DOM in one line. The browser does the parsing for you.

It is also the source of every cross-site scripting (XSS — cross-site scripting, where attacker-controlled HTML or JavaScript runs inside your page) bug ever shipped. Because the string is parsed as HTML, any tags inside it become real elements. If the string came from a user, the user's tags show up too.

broken.js
// DO NOT do this with anything a user typed:
const search = location.hash.slice(1); // "<img src=x onerror=alert(1)>"
results.innerHTML = 'You searched for: ' + search;
// the browser parses the string. The img runs the onerror. Game over.

The fix is almost always: use textContent for user-supplied text, use innerHTML only for strings you wrote yourself. Lesson 10 deals with the cases where you genuinely need user content rendered with formatting (e.g. a comment with allowed bold and italic). For now, default to text.

A useful picture: textContent is a typewriter — whatever you put in, you get out, characters and all. innerHTML is a full HTML interpreter — it reads its input as code. You only hand input to the interpreter that you trust.

Watch out

Reading innerHTML is fine. Writing innerHTML with a string that includes any user input is the line you don't cross. If the rule is "never do it", you cannot get it wrong.

check your understanding
You build an admin tool. The HTML you assemble in code includes a username pulled from a database. You write panel.innerHTML = `<h3>Welcome ${user.name}</h3>`. A new admin signs up with the name <img src=x onerror="fetch('/wipe')">. What happens the next time someone opens that panel?

Attributes vs properties

This is the single most confusing distinction in the DOM, and once you see it once, the rest of your DOM career is easier.

Every HTML attribute (the key="value" part of a tag) is a string in the source. The browser parses it and creates a matching property on the element object — but the property is not always the same type as the attribute. Sometimes it is normalized; sometimes the property is a richer object.

attrs.js
const link = document.querySelector('a');

// Attribute: the literal string in the HTML source
link.getAttribute('href'); // "/about"

// Property: the parsed value on the element object
link.href; // "https://example.com/about" — fully resolved URL

The attribute is what the page started with. The property is what the live DOM reports right now, often after the browser normalized it. For href, the attribute is a relative path; the property is the absolute URL. For value on an <input>, the attribute is the initial value; the property is the current value the user has typed in.

The rule: read and write properties for current state. Use getAttribute / setAttribute only when you specifically need the attribute itself — usually for custom or non-standard names like data-*, aria-*, or role.

value-bug.js
const input = document.querySelector('#email');

// reflects the current input — what the user typed
console.log(input.value);

// reflects only the initial value attribute, not what the user typed
console.log(input.getAttribute('value'));

That gap is a classic bug. New developers reach for getAttribute('value') to read what the user typed, and get whatever the input started as. The current value lives on the property, not the attribute.

check your understanding
An input starts with value="Maya" in the HTML. The user erases it and types "Sam". You read input.getAttribute('value'). What does it return?

The dataset shortcut

Custom attributes on HTML elements have to start with data-. That is the rule that lets the spec evolve without colliding with your own names.

dataset.html
<button data-user-id="42" data-role="admin">Promote</button>

You can read these with getAttribute('data-user-id'), but the DOM gives you a friendlier shortcut: every element has a dataset property that exposes all its data-* attributes as camelCased keys.

dataset.js
const btn = document.querySelector('button');

console.log(btn.dataset.userId); // "42"
console.log(btn.dataset.role);   // "admin"

btn.dataset.lastSeen = '2024-09-01';
// adds data-last-seen="2024-09-01" to the element

data-user-id becomes dataset.userId. data-last-seen becomes dataset.lastSeen. The dashes drop and the next letter capitalizes. Reading and writing dataset is the same as reading and writing the underlying data-* attributes — they stay in sync.

This is the standard place to stash small bits of per-element state — an item id, a row's database key, a tab's panel name. The data sits on the element where it belongs, and your event handlers can pull it off without a separate lookup.

Tip

Values on dataset are always strings. A number like 42 comes back as "42". Convert with Number(btn.dataset.userId) when you need it as a number.

Working with classes

The last bit of read-and-change is the one your CSS will lean on most: classes. The className property gives you the whole class string, but you almost never want to manipulate that by hand. The right tool is classList.

classes.js
const btn = document.querySelector('button');

btn.classList.add('busy');         // add a class
btn.classList.remove('busy');      // remove it
btn.classList.toggle('open');      // flip whether it's there
btn.classList.contains('admin');   // boolean check

classList is the small API that handles the four things you actually do with classes. It does not duplicate, does not produce double spaces, and does not silently overwrite the rest of the classes on the element. Use it.

check your understanding
You have a button with class="primary big". You write btn.className = 'busy'. What is the button's class attribute afterwards?
check your understanding
You want to render a search-result snippet that came from your own server-rendered template (you trust it) into a results panel, including its bold and link tags. Which is the right tool?
← prevnext lesson →
KeepLearningcertificate
for completing
DOM and events
0 of 10 read