Reading and updating content safely
textContent versus innerHTML versus form properties, attribute and class handling, and the script-injection bug that comes with the powerful option.
Choosing the right property
| Property | Gives you | Use it for |
|---|---|---|
textContent | raw text; markup is shown literally | anything a user typed |
innerText | rendered text; respects CSS, forces layout | measuring what is visible |
innerHTML | parsed markup, serialised back on read | trusted templates only |
value | the current form control value | input, textarea, select |
dataset | the data-* attributes as an object | state shared with CSS |
title.textContent = user.name; // safe: rendered as plain text
badge.dataset.state = 'ready'; // writes <span data-state="ready">
output.value = String(total); // form controls use value, not textContent
note.insertAdjacentText('beforeend', ' ok');
// reading back
badge.dataset.state; // 'ready'
select.options[select.selectedIndex].text;⚠️
Assigning untrusted input to
innerHTML turns a comment box into script execution. Use textContent, or sanitise the string with a maintained library first; building HTML by concatenating user data is the same bug with extra steps.Attributes, classes and inline styles
link.setAttribute('href', '/docs');
link.getAttribute('aria-expanded'); // null when the attribute is absent
link.hasAttribute('hidden');
link.removeAttribute('hidden');
// toggle takes a second argument that forces the state
panel.classList.toggle('is-open', isOpen);
panel.classList.replace('old', 'new');
panel.classList.contains('is-open');
// styles: prefer classes, use custom properties for values computed at runtime
panel.style.setProperty('--hue', hue + 'deg');- Attributes are always strings; DOM properties are typed.
input.valueis a string, whileinput.checkedis a boolean. classListmethods are safer than assigningclassName, which silently wipes every other class on the element.- Only
aria-*anddata-*attributes accept names you invent; other made-up attributes are invalid HTML. - Reading a layout property such as
offsetHeightforces the browser to compute styles, so do not interleave reads and writes in a tight loop.
Running your code at the right time
function boot() {
const list = document.querySelector('#todos'); // null if this ran too early
if (!list) return;
list.addEventListener('click', onClick);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot); // HTML parsed
} else {
boot(); // already parsed: the script was deferred or loaded late
}DOMContentLoadedfires when the HTML is parsed, without waiting for images or fonts.loadonwindowwaits for every subresource, which is later and usually unnecessary.- A module script is deferred by default and executes in document order; a classic script in the head executes before the body exists.
- Calling an initialiser twice attaches every listener twice, so guard the entry point against a repeated call.
FAQ
textContent or innerText?
textContent is fast and returns every text node verbatim, including hidden ones. innerText reflects what is rendered, respects CSS and forces a layout pass, so reach for it only when you need the visible text.How do I update one row in a long list?
Change that node. Rewriting a parent with
innerHTML destroys focus, scroll position and the event listeners attached to its children, and it recreates every element.Related
Selecting elements Creating, removing and traversing
Last refreshed 2026-09-18.