Manipulating DOM Elements

1

Manipulating DOM Elements

▶

Once you have selected elements from the DOM, the real work begins: reading what those elements contain and dynamically changing their content, appearance, and behavior. JavaScript exposes a rich set of properties and methods directly on every element object, giving you fine-grained control over text, markup, attributes, CSS classes, inline styles, computed styles, and element-specific properties like form values. Understanding exactly which tool to reach for — and why — is essential to writing correct, secure, and performant DOM code.

Reading and Writing Text Content

Every element in the DOM has several ways to read and write the text or markup it contains. The three most commonly used are textContent, innerHTML, and innerText, and they differ in important ways.

textContent is the simplest and most performant option when you only care about plain text. It returns a string containing the concatenated text of an element and all its descendants, with all HTML tags stripped away. When you set textContent, any existing child nodes are replaced by a single text node, and any characters that would normally be interpreted as HTML (such as < and >) are automatically escaped, making it safe against injection attacks.

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

// Reading text
console.log(heading.textContent); // "Welcome to the Site"

// Writing text — any HTML tags are escaped, not rendered
heading.textContent = 'Hello, <World>!';
// The browser displays the literal string: Hello, !

innerHTML is more powerful but also more dangerous. It gets or sets the full HTML markup inside an element. When you read it, you receive a string of HTML; when you set it, the browser parses and renders the string as actual HTML, creating real child nodes.

const container = document.querySelector('#message');

// Reading markup
console.log(container.innerHTML); // "<strong>Bold text</strong>"

// Writing markup — tags are parsed and rendered
container.innerHTML = '<em>Italic content</em> with a <a href="#">link</a>';

The critical danger with innerHTML arises when you insert user-supplied data. A malicious user could craft a string containing executable script tags or event handler attributes, leading to cross-site scripting (XSS). Never insert raw, unsanitized user input using innerHTML. Use a dedicated sanitization library such as DOMPurify, or prefer textContent when you only need to display text.

// DANGEROUS — never do this with user input:
const userInput = '<img src="x" onerror="stealCookies()">';
container.innerHTML = userInput; // executes attacker code

// SAFE alternative for plain text:
container.textContent = userInput; // displays the raw string literally

innerText behaves similarly to textContent but with two important differences. First, it is layout-aware: it respects CSS rules such as display: none and visibility: hidden, omitting text from hidden elements. Second, it triggers a browser layout (reflow) to calculate what is actually visible, making it noticeably slower. Use innerText when you specifically need to match what a user would see if they selected and copied text from the page; use textContent in all other cases.

Property Parses/Renders HTML Respects CSS Visibility XSS Safe when Setting Performance
textContent No No Yes Fast
innerHTML Yes No No (requires sanitization) Moderate
innerText No Yes Yes Slow (triggers reflow)

Modifying HTML Attributes

HTML attributes are the name-value pairs declared in your markup, such as id, class, href, src, or disabled. JavaScript provides three dedicated methods for working with them that treat everything as a string and operate directly on the element's attribute list.

getAttribute retrieves the current string value of the named attribute. If the attribute does not exist on the element, it returns null.

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

console.log(link.getAttribute('href')); // "/about" (relative, as written in HTML)
console.log(link.getAttribute('data-id')); // "42"
console.log(link.getAttribute('nonexistent')); // null

setAttribute adds a new attribute or replaces the value of an existing one. Both arguments must be strings (or will be coerced to strings).

const img = document.querySelector('img');

img.setAttribute('alt', 'A golden retriever playing fetch');
img.setAttribute('width', '300'); // coerced to the string "300"

removeAttribute deletes the attribute entirely from the element, as if it were never written in the HTML. This is especially important for boolean attributes.

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

btn.setAttribute('disabled', '');    // disables the button
btn.removeAttribute('disabled');     // re-enables it — setting to '' alone won't enable it

Boolean attributes like disabled, checked, readonly, and hidden are special: their presence in the markup activates the behavior, regardless of value. Use setAttribute('disabled', '') to activate and removeAttribute('disabled') to deactivate. Do not try to set setAttribute('disabled', 'false') — the attribute still exists with the string "false", and the button remains disabled.

Working with the classList API

Directly manipulating the className string is error-prone when multiple classes are involved. The classList API provides a clean, dedicated interface for adding, removing, toggling, and checking individual CSS classes without affecting any others on the element.

classList.add appends one or more class names. If a class already exists, it is not duplicated.

const card = document.querySelector('.card');

card.classList.add('highlighted');
card.classList.add('featured', 'active'); // multiple classes at once

classList.remove removes the specified class. If the class is not present, no error is thrown.

card.classList.remove('highlighted');
card.classList.remove('featured', 'active'); // remove multiple at once

classList.toggle is especially useful for interactive UI. It adds the class if it is absent, or removes it if it is present. An optional second boolean argument forces the behavior: pass true to always add, false to always remove.

const menuBtn = document.querySelector('#menu-btn');
const nav = document.querySelector('nav');

menuBtn.addEventListener('click', () => {
  nav.classList.toggle('open'); // open ↔ closed on each click
});

// Force a specific state:
nav.classList.toggle('open', true);  // always adds 'open'
nav.classList.toggle('open', false); // always removes 'open'

classList.contains returns true if the element currently has the class, or false otherwise. This is useful for conditional logic.

if (card.classList.contains('active')) {
  console.log('Card is active');
} else {
  console.log('Card is inactive');
}

Directly Modifying Inline Styles

Every element exposes a style property that is a CSSStyleDeclaration object representing the element's inline styles — the styles written directly in its style attribute. You can read and write individual CSS properties through this object.

Because JavaScript identifiers cannot contain hyphens, CSS property names in kebab-case are converted to camelCase: background-color becomes backgroundColor, font-size becomes fontSize, border-top-width becomes borderTopWidth, and so on.

const box = document.querySelector('.box');

box.style.backgroundColor = '#4a90e2';
box.style.fontSize = '18px';
box.style.marginTop = '2rem';
box.style.display = 'flex';

Setting a style property to an empty string removes that inline style declaration from the element, allowing any stylesheet rules to take effect again.

// Override with inline style
box.style.color = 'red';

// Later, remove the override — the CSS cascade takes over
box.style.color = '';

One crucial limitation: reading element.style.someProperty only reflects styles set inline on the element itself. If a style is applied through a stylesheet rule or inherited from a parent, element.style will return an empty string for that property. For example:

// stylesheet: .box { font-size: 16px; }
const box = document.querySelector('.box');
console.log(box.style.fontSize); // "" — empty, even though 16px is applied via stylesheet

Reading Computed Styles

To read the actual, final styles applied to an element — accounting for stylesheets, inheritance, the cascade, and browser defaults — you must use window.getComputedStyle(). It returns a read-only CSSStyleDeclaration object containing all resolved style values for the element.

const box = document.querySelector('.box');
const computed = window.getComputedStyle(box);

console.log(computed.fontSize);       // "16px" — resolved absolute value
console.log(computed.display);        // "flex"
console.log(computed.backgroundColor); // "rgb(74, 144, 226)"

You can also access values using getPropertyValue(), which accepts the kebab-case CSS property name as a string. This is useful when the property name is dynamic or contains vendor prefixes.

const fontSize = computed.getPropertyValue('font-size');    // "16px"
const bgColor  = computed.getPropertyValue('background-color'); // "rgb(74, 144, 226)"

An important behavioral detail: computed values are always expressed in absolute units. Even if the stylesheet uses em, rem, vw, or percentages, getComputedStyle resolves them to pixel values. This makes it reliable for measurements but means you cannot retrieve the original authored unit.

// stylesheet: h1 { font-size: 2rem; }
const h1 = document.querySelector('h1');
const styles = window.getComputedStyle(h1);
console.log(styles.fontSize); // "32px" — resolved from 2rem, not "2rem"

You can also pass an optional second argument (a pseudo-element string such as '::before') to read the computed styles of a CSS pseudo-element.

const before = window.getComputedStyle(box, '::before');
console.log(before.content); // e.g., '"★"'

Getting and Setting Element Properties

Beyond generic attribute methods, DOM elements expose many of their attributes as direct JavaScript properties on the element object. These are live, typed JavaScript values rather than raw strings, and they often provide richer or more convenient behavior.

Common identity properties include id, tagName, and className. Reading tagName returns the element's tag in uppercase (e.g., "DIV", "BUTTON"), and you can freely get and set id and className as strings.

const el = document.querySelector('.card');

console.log(el.id);       // "card-01"
console.log(el.tagName);  // "DIV"
console.log(el.className); // "card featured"

el.id = 'card-new';
el.className = 'card archived'; // replaces ALL classes at once

For form elements, the value property is the primary way to read and write the current content of an <input>, <textarea>, or <select>. Unlike the value attribute (which reflects the initial value set in HTML), the property reflects what the user has currently typed or what you have set programmatically.

const nameInput = document.querySelector('#name');

// Read what the user typed
console.log(nameInput.value); // "Alice"

// Programmatically set the field's value
nameInput.value = 'Bob';

// Clear the field
nameInput.value = '';

Checkbox and radio inputs expose a checked boolean property. It returns true if the input is currently selected and false otherwise, and you can set it programmatically without triggering a change event.

const checkbox = document.querySelector('#agree');

console.log(checkbox.checked); // true or false

// Programmatically check it
checkbox.checked = true;

// Uncheck it
checkbox.checked = false;

For anchor and image elements, there is a meaningful distinction between the attribute and the property for href and src. The attribute stores whatever string was written in the HTML (which may be a relative path), while the property returns the fully resolved absolute URL.

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

// Suppose the HTML is: <a href="/about">About</a>
console.log(link.getAttribute('href')); // "/about"      — relative, as authored
console.log(link.href);                 // "https://example.com/about" — fully resolved

const img = document.querySelector('img');
// Suppose: <img src="images/photo.jpg">
console.log(img.getAttribute('src')); // "images/photo.jpg"
console.log(img.src);                 // "https://example.com/images/photo.jpg"

This distinction matters when you need to compare or process URLs: use the property (link.href) when you need the absolute URL, and use getAttribute('href') when you need the raw, as-authored value for tasks like serialization or relative-path logic.

The following table summarizes the key DOM manipulation tools covered in this topic:

Tool Purpose Key Consideration
textContent Read/write plain text of an element Escapes HTML; safe; fast
innerHTML Read/write HTML markup inside an element Parses HTML; XSS risk with user data
innerText Read/write visible text (CSS-aware) Triggers reflow; slower
getAttribute Read an attribute as a string Returns null if absent
setAttribute Add or update an attribute Values coerced to string
removeAttribute Delete an attribute entirely Required to deactivate boolean attributes
classList.add/remove/toggle/contains Manage individual CSS classes Does not clobber other classes
element.style.property Read/write inline styles Only reflects inline styles; camelCase names
getComputedStyle() Read all resolved styles from cascade Read-only; values in absolute units
element.value Current value of form fields Reflects live user input, not HTML default
element.checked Boolean state of checkbox/radio Can be set programmatically
element.href / element.src Resolved absolute URL of link/image Use getAttribute for the raw authored path
NotesCovers all listed subtopics in depth with code examples for each property and method. The XSS risk of innerHTML is explicitly addressed with a code example. Comparison tables are provided for text content properties and for the overall summary of tools. The attribute vs. property distinction for href/src is illustrated with concrete examples.