1Modifying Existing Elements and Their Properties
▶
One of the most fundamental tasks in client-side JavaScript is reaching into a live web page and changing what the user sees or how elements behave — without reloading the page. The Document Object Model (DOM) exposes a rich set of properties and methods that let you read and write virtually every characteristic of an element: its attributes, its styles, its classes, its text, and its dimensions. Mastering these techniques is the foundation of interactive web development, because nearly every UI feature — toggling a menu, disabling a button after a form submits, swapping an image, showing a validation error — relies on modifying existing elements at runtime.
This topic walks through each category of modification in depth, explains the subtle but important distinctions between similar-looking APIs (such as getAttribute vs. direct property access, or textContent vs. innerText vs. innerHTML), and supplies concrete examples you can reason about and adapt.
Reading and Writing Element Attributes
HTML elements carry attributes — the key-value pairs written directly in markup, like src="photo.jpg" or disabled. The DOM provides three dedicated methods for working with these attributes as strings, regardless of what kind of attribute they are.
element.getAttribute('src') reads the current serialized value of any attribute. It always returns a string (or null if the attribute does not exist), and it never alters the element. This is the safe, universal way to inspect any attribute, including non-standard data attributes and ARIA attributes:
const img = document.querySelector('img');
const currentSrc = img.getAttribute('src');
console.log(currentSrc); // e.g. "images/hero.jpg"
const role = img.getAttribute('aria-label');
console.log(role); // null if not present
element.setAttribute('href', 'https://example.com') creates the attribute if it does not already exist, or replaces its value if it does. The second argument is always coerced to a string:
const link = document.querySelector('a');
link.setAttribute('href', 'https://example.com');
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
element.removeAttribute('disabled') deletes the attribute entirely from the element. This matters especially for boolean attributes like disabled, checked, or readonly, where the presence of the attribute — not its value — determines the behavior. Removing disabled from a form control re-enables it for user interaction:
const btn = document.querySelector('#submit-btn');
btn.setAttribute('disabled', ''); // disables the button
// ... later, after validation passes:
btn.removeAttribute('disabled'); // re-enables it
Direct property access is a shorthand that works for reflected IDL attributes — attributes that the browser automatically mirrors as JavaScript properties on the element object. For example, element.id, element.className, element.href, and input.value are all reflected properties:
const heading = document.querySelector('h1');
heading.id = 'main-title'; // same effect as setAttribute('id', 'main-title')
console.log(heading.id); // 'main-title'
The important distinction is that properties and attributes can diverge. After a user types into an input, input.value (the property) reflects what is currently in the field, while input.getAttribute('value') still returns the original default from the HTML. For URLs, anchor.href (property) returns the fully resolved absolute URL, whereas anchor.getAttribute('href') returns whatever string was written in the markup. Use getAttribute when you need the raw authored value; use properties for live, reflected state.
Modifying Inline Styles
Every element has a style property that exposes its inline style declarations — the ones that would appear inside a style="..." attribute in markup. You read and write individual CSS properties through camelCased JavaScript property names:
const box = document.querySelector('.box');
box.style.color = 'red';
box.style.fontSize = '18px'; // hyphenated CSS → camelCase JS
box.style.backgroundColor = '#f0f0f0';
box.style.borderRadius = '8px';
element.style.display = 'none' is one of the most common operations in DOM scripting — it hides an element by removing it from the layout flow entirely. To make it visible again, assign a meaningful display value:
const modal = document.querySelector('#modal');
modal.style.display = 'none'; // hide
// show again:
modal.style.display = 'flex'; // or 'block', 'grid', etc.
When you need to apply several style declarations at once, element.style.cssText lets you write them as a single CSS string. Note that assigning cssText replaces the entire inline style, discarding any previously set inline declarations:
box.style.cssText = 'color: red; font-size: 16px; margin: 0 auto;';
A crucial limitation: reading element.style.propertyName only reflects styles that were set inline — either in the style attribute in markup or via JavaScript. It returns an empty string for any property controlled by an external stylesheet or a <style> block:
// If CSS file says: p { color: blue; }
const para = document.querySelector('p');
console.log(para.style.color); // '' — empty! Not visible here.
To actually read the rendered value, you need computed styles (covered next).
Reading Computed Styles
The browser's cascade merges rules from every stylesheet, inherited properties, and browser defaults into a final set of computed values for every element. getComputedStyle(element) returns a live CSSStyleDeclaration object representing those resolved values:
const para = document.querySelector('p');
const styles = getComputedStyle(para);
console.log(styles.getPropertyValue('font-size')); // e.g. "16px"
console.log(styles.getPropertyValue('color')); // e.g. "rgb(33, 33, 33)"
console.log(styles.getPropertyValue('line-height')); // e.g. "24px"
You can also access properties directly using camelCase notation on the returned object:
console.log(styles.fontSize); // "16px"
console.log(styles.backgroundColor); // "rgba(0, 0, 0, 0)" if transparent
Three important rules about computed styles:
- Read-only. Attempting to assign a value through the
getComputedStyleresult throws an error. To change a style, useelement.styleor manipulate classes. - Absolute units. All length values are resolved to pixels, even if the original CSS used
em,rem,%, orvw. This makes computed styles reliable for layout calculations. - Pseudo-elements. You can pass a second argument to inspect pseudo-element styles:
getComputedStyle(element, '::before').
// Practical example: check if an element is visible
function isVisible(el) {
const cs = getComputedStyle(el);
return cs.display !== 'none' &&
cs.visibility !== 'hidden' &&
cs.opacity !== '0';
}
Manipulating CSS Classes
Rather than hard-coding styles in JavaScript, the preferred approach is to define visual states as CSS classes and then add or remove those classes with JavaScript. This cleanly separates presentation from behavior. The classList API makes this ergonomic and reliable.
element.classList.add('active') appends a class name to the element's class list. If the class is already present, it is not duplicated:
const nav = document.querySelector('nav');
nav.classList.add('active');
nav.classList.add('highlighted', 'visible'); // multiple classes at once
element.classList.remove('hidden') removes a class without affecting any others. If the class is not present, no error is thrown:
nav.classList.remove('hidden');
nav.classList.remove('loading', 'error'); // remove multiple at once
element.classList.toggle('open') is a convenient single call for show/hide or expand/collapse patterns. It adds the class if absent, removes it if present, and returns a boolean indicating the new state:
const menuBtn = document.querySelector('#menu-btn');
const menu = document.querySelector('#menu');
menuBtn.addEventListener('click', () => {
const isOpen = menu.classList.toggle('open');
menuBtn.setAttribute('aria-expanded', isOpen);
});
You can pass a second boolean argument to force a specific state: classList.toggle('open', true) always adds; classList.toggle('open', false) always removes.
element.classList.contains('selected') returns true or false, allowing conditional logic:
if (card.classList.contains('selected')) {
submitSelection(card);
} else {
card.classList.add('selected');
}
element.classList.replace('old-class', 'new-class') atomically swaps one class for another, which is tidier than a remove-then-add pair. It returns true if the replacement succeeded (i.e., the old class was present), or false if it was not found:
const result = btn.classList.replace('loading', 'success');
if (!result) {
// 'loading' wasn't there; handle accordingly
}
Updating Text and HTML Content
Three properties let you read or overwrite what is inside an element, and choosing the right one matters for both correctness and security.
element.textContent treats everything as plain text. Assigning to it removes all existing child nodes and inserts a single text node. Any HTML tags in the assigned string are rendered as literal characters, not parsed as markup — making it inherently safe against injection attacks:
const div = document.querySelector('#message');
div.textContent = 'Hello, World!';
// Safe even with user-supplied data:
const userInput = '<script>alert("xss")</script>';
div.textContent = userInput; // displays the raw string, does NOT execute
element.innerText is superficially similar but has key differences: it is layout-aware, meaning it only returns text that is actually rendered and visible. Hidden elements (via display: none) are excluded. It also normalizes whitespace according to CSS rendering rules and respects line breaks. Reading innerText forces the browser to perform a layout calculation, making it noticeably slower than textContent. Prefer textContent when you only need raw string content; use innerText only when you specifically need the user-visible text:
// textContent includes text in hidden children; innerText does not
const container = document.querySelector('#info');
console.log(container.textContent); // all text, including from display:none children
console.log(container.innerText); // only visible text
element.innerHTML parses the assigned string as HTML and replaces the element's children with the resulting node tree. This is powerful but dangerous with untrusted data. Always sanitize user-supplied strings before passing them to innerHTML:
const article = document.querySelector('article');
// Reading: get serialized HTML of children
const backup = article.innerHTML;
// Writing: parse and insert markup
article.innerHTML = '<p>Updated <strong>content</strong> here.</p>';
// DANGER — never do this with user data:
// article.innerHTML = userSuppliedString; // XSS vulnerability
Reading element.innerHTML returns the browser's serialization of the current child nodes as an HTML string, useful for inspecting or temporarily saving markup before you modify it.
Modifying Element Properties Directly
Many element types expose purpose-specific properties that map to their key behaviors. These are the most direct and idiomatic way to control form controls, images, and links.
inputElement.value gets or sets the current value of a text, number, or textarea field. This is the live value — it reflects what the user has typed, not the initial value attribute:
const input = document.querySelector('#username');
input.value = 'Default text'; // pre-fill the field
console.log(input.value); // read whatever is currently in the field
// Clear a field:
input.value = '';
imageElement.src reassigns the image source. The browser immediately initiates a network request for the new URL and displays the image when it loads. This is the basis of image sliders, lazy-loading, and avatar updates:
const avatar = document.querySelector('#avatar');
avatar.src = 'images/user-42.png';
// Handle load/error:
avatar.addEventListener('error', () => {
avatar.src = 'images/default-avatar.png';
});
buttonElement.disabled is a boolean property. Setting it to true adds the disabled attribute and prevents all user interaction (clicks, keyboard activation). Setting it to false removes the attribute and restores interactivity:
const submitBtn = document.querySelector('#submit');
submitBtn.disabled = true; // prevent double-submit
// ... after async operation completes:
submitBtn.disabled = false;
anchorElement.href changes where a link navigates. You can assign either an absolute or relative URL. The property always reflects the absolute resolved URL, which is distinct from the raw string stored in the attribute:
const link = document.querySelector('#next-page');
link.href = '/chapter-2'; // relative URL
console.log(link.href); // "https://example.com/chapter-2" (absolute)
console.log(link.getAttribute('href')); // "/chapter-2" (as written)
Changing Element Dimensions and Position via Style
JavaScript can control layout-related CSS properties through the same element.style interface described earlier. Setting explicit dimensions is straightforward:
const panel = document.querySelector('.panel');
panel.style.width = '300px';
panel.style.height = '150px';
panel.style.overflow = 'hidden';
Precise positioning uses position in combination with offset properties. The element's position is calculated relative to its nearest positioned ancestor (one with position set to anything other than static):
const tooltip = document.querySelector('#tooltip');
tooltip.style.position = 'absolute';
tooltip.style.top = '40px';
tooltip.style.left = '120px';
tooltip.style.zIndex = '100';
element.style.transform is the performance-preferred way to visually move or scale elements. CSS transforms operate on the compositor thread, which means they are GPU-accelerated and do not trigger a full layout reflow of the page. This makes them far smoother for animations than changing top or left directly:
// Move 100px to the right — no reflow triggered:
el.style.transform = 'translateX(100px)';
// Combine multiple transforms:
el.style.transform = 'translateX(50px) scale(1.2) rotate(15deg)';
// Animate with a transition:
el.style.transition = 'transform 0.3s ease';
el.style.transform = 'translateX(200px)';
Centering a block element horizontally with JavaScript mirrors the classic CSS technique:
const centered = document.querySelector('.centered');
centered.style.display = 'block';
centered.style.width = '500px';
centered.style.margin = 'auto'; // browser splits remaining space equally left and right
The table below summarizes the key APIs covered in this topic, their purpose, and the most important caveat for each:
| API | Category | Purpose | Key Caveat |
|---|---|---|---|
getAttribute / setAttribute / removeAttribute |
Attributes | Read, write, or delete any HTML attribute as a string | Always returns strings; use for non-reflected or data attributes |
element.id, element.href, etc. |
Attributes | Direct property access for reflected IDL attributes | Property and attribute values can diverge (e.g., href absolute vs. raw) |
element.style.property |
Inline Styles | Read or set individual inline CSS declarations | Only reflects inline styles, not stylesheet rules |
element.style.cssText |
Inline Styles | Apply multiple inline declarations at once | Replaces entire inline style string |
getComputedStyle(element) |
Computed Styles | Read the final resolved style values the browser is using | Read-only; values always in absolute units (pixels) |
classList.add / remove / toggle / contains / replace |
Classes | Manage CSS class membership cleanly | Preferred over className for targeted manipulation |
element.textContent |
Content | Set or read element text safely (no HTML parsing) | Ignores CSS visibility; safe for user data |
element.innerText |
Content | Read visible rendered text only | Triggers layout; slower than textContent |
element.innerHTML |
Content | Parse and insert HTML markup as children | XSS risk with untrusted input; never use with raw user data |
input.value, img.src, btn.disabled |
Element Properties | Control live behavior of specific element types | Operates on current state, not the initial HTML attribute |
element.style.transform |
Dimensions / Position | Move or scale elements without causing layout reflow | GPU-accelerated; preferred for animations over top/left |