JAVASCRIPT / THE BROWSER: DOM, EVENTS, AND STORAGE
Styling elements through classes and inline rules
Change how an element looks by toggling classes with classList or writing inline declarations with element.style, and know which one wins in the cascade.
What you will learn
- Toggle state classes with classList.add/remove/toggle rather than rewriting className
- Pass a second boolean to classList.toggle to force a class on or off
- Use getComputedStyle to read effective styles; el.style shows only inline ones
- Write units in inline values ('4px') and set --custom props with setProperty
Understanding Styling elements through classes and inline rules
Two different attributes control how an element looks, and the DOM gives you a separate handle for each one. classList and className edit the class attribute, which decides which rules in your stylesheets match the element; element.style edits the style attribute, which is a declaration block belonging to that single element. Toggling a class never changes a stylesheet rule, it only changes whether the element qualifies for rules that already exist.
element.style is a window onto the inline attribute and nothing else, which trips up people who use it to read styles. If a stylesheet says .card { color: teal }, then card.style.color is the empty string, because nothing was ever written into that element's style attribute. getComputedStyle(card).color reports what the browser actually resolved, as rgb(0, 128, 128), and that object is read-only, so it answers questions but never sets anything.
The cascade settles the conflict when both handles are in play: a declaration in the style attribute beats every normal rule from a stylesheet, no matter how specific the selector is. That is why one stray el.style.display = 'none' makes a later classList.add('visible') look broken, and why the repair is el.style.display = '' rather than more CSS. So name your visual states as classes, keeping the design in the stylesheet where transitions and media queries still reach it, and reserve inline declarations for values you can only compute at runtime, ideally passing them to CSS as custom properties.
const box = document.createElement('div');
box.className = 'card'; // a plain string assignment, it wipes anything before it
box.classList.add('active', 'card'); // 'card' is already present, so it is not repeated
console.log(box.className);
const stillActive = box.classList.toggle('active');
console.log(stillActive, box.classList.length);
box.style.backgroundColor = 'teal'; // camelCase property name
box.style.padding = '4px'; // string value, unit included
console.log(box.getAttribute('style'));
console.log(box.style.color === ''); // never set inline, so it reads as emptyclassList changes which stylesheet rules match an element, while element.style writes a per-element declaration block that outranks those rules.
Worked examples
Inline beats the class rule
Shows that a declaration written through element.style outranks a stylesheet rule, and that clearing it hands control back.
const sheet = document.createElement('style');
sheet.textContent = '.warn { color: rgb(200, 0, 0); font-weight: 700; }';
document.head.appendChild(sheet);
const p = document.createElement('p');
p.className = 'warn';
p.textContent = 'disk almost full';
document.body.appendChild(p);
console.log(getComputedStyle(p).color);
console.log('inline:', JSON.stringify(p.style.color));
p.style.color = 'rgb(0, 0, 255)';
console.log(getComputedStyle(p).color);
p.style.color = '';
console.log(getComputedStyle(p).color);Example explained
Line 1The .warn rule matches because of the class attribute, so getComputedStyle reports rgb(200, 0, 0).
Line 2p.style.color is still empty: the color came from a rule, not from the style attribute.
Line 3Assigning p.style.color adds a declaration to the style attribute, which outranks the .warn rule regardless of selector specificity.
Line 4Assigning the empty string deletes that declaration, and the class rule applies again without touching any CSS.
Forcing a toggle, and replacing a token
Demonstrates the second argument of classList.toggle and the boolean returned by classList.replace.
const btn = document.createElement('button');
btn.className = 'btn btn-idle';
console.log(btn.classList.toggle('busy', true));
console.log(btn.classList.toggle('busy', true));
console.log(btn.className);
console.log(btn.classList.replace('btn-idle', 'btn-busy'));
console.log(btn.classList.replace('btn-idle', 'btn-x'));
console.log(btn.className);Example explained
Line 1toggle('busy', true) behaves like add: it returns true and a second call does not flip the class back off, which is what you want when the class mirrors a boolean in your data.
Line 2className lists the tokens in insertion order, and btn-idle was left alone by the toggle.
Line 3replace returns true because btn-idle was found, and the new token takes the old one's position.
Line 4The second replace returns false and changes nothing, since btn-idle no longer exists on the element.
Custom properties, units, and cssText
Covers setProperty for dashed names, the silent rejection of unitless numbers, and how cssText wipes the whole style attribute.
const bar = document.createElement('div');
bar.style.setProperty('--fill', '35%');
bar.style.setProperty('width', 'var(--fill)');
console.log(bar.style.getPropertyValue('--fill'));
console.log(bar.style.width);
bar.style.height = 10;
console.log(bar.style.height === '');
bar.style.cssText = 'height: 10px';
console.log(bar.getAttribute('style'));Example explained
Line 1--fill has to go through setProperty, because a dashed custom property has no camelCase name on the style object.
Line 2Reading back style.width returns the var(--fill) text: substitution happens at render time, not at assignment time.
Line 3Assigning the number 10 to style.height stringifies to '10', which is not a valid length, so the CSSOM drops it with no error and height stays empty.
Line 4Writing cssText rebuilds the whole style attribute, so the earlier --fill and width declarations are gone.
Important notes
getComputedStyle is read-only and normalizes values, returning rgb(...) instead of a color name and px instead of em, so comparing it against the string you wrote will often fail.
Dot notation needs camelCase (style.borderTopWidth); hyphenated and custom names need brackets or setProperty, as in style['border-top-width'] and style.setProperty('--gap', '8px').
Common mistakes
Adding a class with el.className = 'active', which deletes every class the element already had, or with el.className += 'active', which produces 'cardactive' because the space is missing.
Assigning a bare number, as in el.style.width = 240: it becomes the string '240', which is not a valid length, so the declaration is silently discarded and the element does not move.
Testing visibility with el.style.display: it only sees the style attribute, so an element hidden by a stylesheet rule reports the empty string and the branch takes the wrong path.
Try it yourself
Change, predict, then run
In a scratch page, write a .night rule that sets background and color, then add a button whose click calls document.body.classList.toggle('night') and uses the returned boolean to relabel itself 'Night on' or 'Night off'. Add a range input that sets a bar's inline width in percent, and confirm with getComputedStyle that its pixel width changes while the class attribute stays untouched.
Open the JavaScript workspaceCheck your understanding
A stylesheet contains .alert { background: red }. Earlier code ran btn.style.background = 'gray'. You now call btn.classList.add('alert') and the button stays gray. Why?
- classList.add silently failed because the element already had a class attribute
- The .alert rule needs a more specific selector to beat a single class
- Inline declarations outrank any selector-based rule, so the gray from style.background still applies
- Stylesheets are matched only while the page loads, so classes added by script never take effect
Show answer
Writing to element.style puts the declaration in the element's style attribute, and the cascade places that above every normal stylesheet rule whatever the selector, so gray keeps winning; specificity never enters into it, because the inline declaration is not a selector at all. Clearing it with btn.style.background = '' lets .alert through. Classes added by script are matched immediately, so the load-time option is simply false.