Selecting an element is only half the job. Once you have it, you can change what it says, what its attributes are and how it looks. This lesson covers each of those, and one rule that protects your users: put text on the page with textContent, not innerHTML. By the end, the quiz app from lesson 14 shows its first real question.
Changing text with textContent
textContent reads or replaces all the text inside an element:
const heading = document.querySelector("h1");
console.log(heading.textContent);
// → JavaScript quiz
heading.textContent = "JavaScript quiz: round 1";
Setting textContent replaces everything inside the element, including any child elements, with a single text node. Whatever string you assign is shown exactly as written. Characters such as < and & appear as themselves; they are never treated as markup.
You may also meet innerText. It returns only the text that is visible after CSS is applied, which forces the browser to calculate the layout. Use textContent unless you specifically need that behaviour.
Why not innerHTML?
innerHTML also changes an element's content, but it parses the string as HTML:
const status = document.querySelector("#status");
status.innerHTML = "<strong>Correct!</strong>";
// The paragraph now contains a strong element.
That looks convenient. The danger appears when any part of the string comes from outside your code: a form field, a URL parameter, a server response or a database. Imagine showing a player's name:
const playerName = '<img src="x" onerror="alert(document.cookie)">';
status.innerHTML = `Well done, ${playerName}`;
// The browser creates an img element. Its image fails to load,
// so the onerror attribute runs the attacker's JavaScript.
This is cross-site scripting (XSS). Script injected this way runs with the full powers of your page. It can read what the user types, change what they see, or send requests as them. Because the text arrives as data, it can come from another user entirely, for example a malicious name saved to a leaderboard and shown to everyone.
Now the same name with textContent:
status.textContent = `Well done, ${playerName}`;
// The page shows the literal text: Well done, <img src="x" onerror=...>
Nothing is parsed, so nothing runs. The name looks odd, but it is harmless.
The rule for this course:
- Use
textContentfor any text, and always for text that did not come from your own source code. - When you need new elements, build them with
document.createElement()and fill them withtextContent. Lesson 28 shows how. - Keep
innerHTMLfor fixed markup you wrote yourself, if you use it at all. Do not try to make untrusted strings safe by removing<script>yourself. There are far too many ways to run script in HTML.
Assigning innerHTML also throws away every existing element inside, along with its event listeners and keyboard focus. Even with trusted markup, it is a blunt tool.
Changing attributes
Most standard HTML attributes are available as properties of the element with the same name:
const link = document.querySelector("a");
link.href = "https://developer.mozilla.org/";
console.log(link.href);
// → https://developer.mozilla.org/
const button = document.querySelector("#next-button");
button.disabled = true; // adds the disabled attribute
button.disabled = false; // removes it
Boolean attributes such as disabled, hidden and checked become true or false properties, which is easier than adding and removing the attribute.
For any attribute, including ones without a property, use the attribute methods:
| Method | What it does |
|---|---|
el.getAttribute("name") | Returns the value as a string, or null if absent |
el.setAttribute("name", "value") | Adds or changes the attribute |
el.removeAttribute("name") | Removes it |
el.hasAttribute("name") | Returns true or false |
ARIA attributes are a common case:
button.setAttribute("aria-disabled", "true");
console.log(button.getAttribute("aria-disabled"));
// → true
Attribute values are always strings. setAttribute("aria-disabled", true) stores the string "true".
data attributes and dataset
Custom data-* attributes store small bits of information on an element. Read and write them through dataset, where data-question-id becomes dataset.questionId:
button.dataset.questionId = "2";
console.log(button.getAttribute("data-question-id"));
// → 2
Showing and hiding
The hidden property hides an element from everyone, including screen reader users, exactly like the HTML hidden attribute:
button.hidden = true; // gone
button.hidden = false; // back
Use it for content that should not exist for anyone at the moment, such as a Next button before an answer has been chosen.
Changing classes with classList
To change how an element looks, change its classes and let your CSS decide what the class means. classList has a method for each job:
const option = document.querySelector(".option");
option.classList.add("is-correct");
option.classList.remove("is-correct");
option.classList.toggle("is-selected"); // add if missing, remove if present
option.classList.toggle("is-selected", false); // force off
console.log(option.classList.contains("option"));
// → true
option.classList.replace("is-wrong", "is-correct");
className replaces the entire class string at once, which can wipe out classes other code relies on. Prefer classList.
Why classes beat inline styles
Every element also has a style property for inline styles:
option.style.outline = "3px solid green";
option.style.backgroundColor = "honeydew"; // CSS names become camelCase
It works, but it scatters design decisions through your JavaScript:
- Inline styles beat almost every rule in your stylesheet, so they are hard to override later, for example in dark mode or a high-contrast theme.
- Someone changing the design has to search the scripts as well as the CSS.
- Removing a state means undoing each property one by one, where removing a class undoes them all.
So keep the look in CSS and let JavaScript switch state classes:
.option.is-correct {
outline: 3px solid green;
}
.option.is-wrong {
outline: 3px solid firebrick;
}
The good use for style is a value that only JavaScript can calculate, such as a position or a percentage. Even then, setting a CSS custom property keeps the styling in CSS:
document.querySelector("#quiz").style.setProperty("--progress", "33%");
The CSS Complete Course covers custom properties.
The quiz: showing a question
Time to put the first question on the page. Add the two CSS rules above inside a <style> element in the head of quiz/index.html. Then add this to quiz/quiz.js, below the selections from lesson 14:
let currentIndex = 0;
function showQuestion(index) {
const question = questions[index];
progress.textContent = `Question ${index + 1} of ${questions.length}`;
questionText.textContent = question.text;
optionButtons.forEach((button, i) => {
button.textContent = question.options[i];
button.classList.remove("is-correct", "is-wrong");
button.removeAttribute("aria-disabled");
});
statusRegion.textContent = "";
nextButton.hidden = true;
}
function markAnswer(button, isCorrect) {
button.classList.add(isCorrect ? "is-correct" : "is-wrong");
statusRegion.textContent = isCorrect
? "Correct!"
: `Not quite. The answer is ${questions[currentIndex].answer}.`;
nextButton.hidden = false;
}
showQuestion(currentIndex);
Reload the page. The progress line, question and three options now come from the questions array. All of it is set with textContent, so even if the questions later come from a server or a form (lesson 17 adds one), a question cannot inject markup.
showQuestion also resets anything an earlier answer changed: classes, aria-disabled, the status message and the Next button. Resetting state in one place is what makes it safe to call again for the next question.
There are no click handlers yet; they arrive in the next lesson. You can try markAnswer from the console:
markAnswer(optionButtons[1], true);
// The second option gets a green outline, the status says "Correct!"
// and the Next button appears.
The outline is not the only signal. The status message says in words whether the answer was right, so the result does not depend on colour, and screen readers announce it because the status paragraph is a live region.
Summary
textContentreads and replaces an element's text, and never parses it as HTML.innerHTMLparses strings as HTML; with untrusted data it allows cross-site scripting, so do not use it for data.- Use properties such as
href,disabledandhiddenwhere they exist, andsetAttributefor the rest, including ARIA. - Read and write
data-*attributes withdataset. - Change appearance by toggling classes with
classList, and keep inline styles for values only JavaScript can calculate.