Attributes, classes and inline styles from script

Attribute versus property, boolean attributes, the dataset naming rules, classList operations, and the difference between inline style and computed style.

Attributes and their properties

const input = document.querySelector('#email');

// attributes: always strings, always what the markup said
input.getAttribute('type');            // "email"
input.setAttribute('maxlength', '40');
input.hasAttribute('required');        // boolean
input.removeAttribute('maxlength');
input.toggleAttribute('required');

// properties: the live, typed value the DOM is actually using
input.type;                            // "email"
input.required;                        // true/false
input.maxLength;                       // number

const link = document.querySelector('a');
link.getAttribute('href');             // "/docs" - as written
link.href;                             // "https://site.test/docs" - resolved
  • For value, checked, selected and href, attribute and property can disagree. The property is what the user sees; the attribute is what the markup said.
  • After a user types into an input, getAttribute("value") still returns the original. Use input.value.
  • Setting the value attribute changes the input's default; setting the property changes the current value.
  • el.attributes is a live NamedNodeMap, useful for copying every attribute onto another element.
// boolean attributes: presence is the value, not the string
const btn = document.querySelector('button');
btn.setAttribute('disabled', 'false');   // still disabled - presence is enough
btn.disabled = false;                    // this is what you meant

btn.removeAttribute('disabled');         // the other correct form

// copy every attribute from one node to another
for (const { name, value } of [...source.attributes]) {
  target.setAttribute(name, value);
}
⚠️
Writing setAttribute("disabled", "false") disables the element. Boolean attributes are true when present regardless of their value, so the only way to turn one off is to remove it - or set the matching property to false.

Dataset and classList

<div id="row"
     data-user-id="42"
     data-role="admin"
     data-gdpr-consent="2026-01-04"></div>
const row = document.querySelector('#row');

row.dataset.userId;       // "42"      data-user-id
row.dataset.role;         // "admin"
row.dataset.gdprConsent;  // "2026-01-04"

row.dataset.userId = '43';            // writes back to the attribute
delete row.dataset.role;              // removes data-role

row.getAttribute('data-user-id');     // still works, same storage
  • Dashes become camelCase: data-user-id is dataset.userId. A name that is not valid in a property path has to be read with getAttribute.
  • Everything is a string. dataset.count is "3", not 3.
  • Prefer data attributes for state that belongs to the markup and must be readable in the DOM inspector; prefer a JS variable for state the user never needs to see.
const el = document.querySelector('.card');

el.classList.add('is-open');
el.classList.remove('is-open');
el.classList.toggle('is-open');            // add or remove, returns the result
el.classList.toggle('is-open', isOpen);    // explicit, idempotent
el.classList.replace('is-open', 'is-closed');
el.classList.contains('is-open');          // boolean

[...el.classList];                         // array of class names
el.className = 'card is-open';             // replaces the whole list at once

classList.toggle(name, force) is the version to use in rendering code: given the same state twice it produces the same result, which matters when the same render function runs more than once.

Inline style versus computed style

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

// inline style: only what was set on the element itself
box.style.backgroundColor = '#0af';        // camelCase property names
box.style.setProperty('--brand', '#0af');
box.style.setProperty('--gap', '12px', 'important');
box.style.removeProperty('--brand');

console.log(box.style.width);              // "" unless set inline
console.log(box.getAttribute('style'));    // the whole inline string

// computed style: the value after the cascade, always resolved
const cs = getComputedStyle(box);
cs.backgroundColor;                        // "rgb(0, 170, 255)"
cs.width;                                  // "320px" - even if set as 20rem
cs.getPropertyValue('--brand').trim();     // custom properties resolve here
cs.marginTop;                              // "0px" even with no rule

// a temporary override without touching the stylesheet
const prev = box.style.display;
box.style.display = 'none';
Read withReturnsUse for
el.style.propOnly the inline valueDetecting what you set yourself
getComputedStyle(el).propResolved used valueLayout, colours, custom properties
el.getAttribute("style")Raw attribute stringCopying inline style verbatim
el.style.setProperty()Sets one declarationCustom properties and important
// reading a custom property set by a stylesheet
const brand = getComputedStyle(document.documentElement)
  .getPropertyValue('--brand').trim();

// then use it in JS that cannot be expressed in CSS
drawChart(brand);

FAQ

Should I set styles with classList or with el.style?
Use classes for anything that is a state or a design decision - they are reusable, reviewable in CSS, and can be overridden by a media query. Use inline style for values that genuinely come from data at runtime, such as a width from a measurement or a colour from a user's preference.
Why is getComputedStyle returning an empty string?
You asked for a property the element does not have, or you used the wrong property name. Computed style uses the CSS names for custom properties but camelCase for standard ones, and an unknown property returns an empty string rather than throwing.

Reading and updating content safely Events and delegation in depth

Last refreshed 2026-09-18.