Why change classes instead of styles?
Most visual changes in an interactive page are a switch between two or three known states: a menu is open or closed, a card is sold out or available, a field is valid or showing an error. Each state already has a look you can describe in CSS. The script's only job is to say which state the element is in, and the cleanest way to say that is a class name.
Compare the two approaches. Setting styles from JavaScript means writing menu.style.display = "grid", menu.style.gap = "8px" and menu.style.marginTop = "12px", then undoing all three when the menu closes. Toggling a class is one line, and the three values sit in the stylesheet next to the rest of the design. A designer can change the gap without opening the script, and media queries, hover rules and dark mode all keep working because the CSS is still in charge.
Keep this split in mind for the whole course: design in CSS, state in classes, and JavaScript as the thing that flips the switch.
/* style.css */
.menu { display: none; }
.menu.open { display: grid; gap: 8px; }
// script.js
menu.classList.add("open");

open class. The return value of toggle tells you the new state, which you copy into aria-expanded so screen readers hear the same thing sighted users see.What can classList do?
Every element has a classList property. It is a live list of the element's classes with methods for changing one class at a time. It never touches the other classes, which is the main reason to prefer it over assigning className.
Each method takes the bare class name, with no dot. The dot belongs to CSS selectors and to querySelector; a class on an element is just the word.
const card = document.querySelector(".loaf");
card.classList.add("sold-out", "faded");
console.log(card.classList.contains("sold-out")); // true
card.classList.toggle("faded");
console.log(card.className); // loaf sold-outel.classList.add("open")adds the class. Adding one that is already there does nothing, so it is safe to call twice.el.classList.remove("open")removes it. Removing a class that is not there is also harmless.el.classList.toggle("open")adds it if missing and removes it if present, and returnstrueif the class is on afterwards.el.classList.toggle("open", isOpen)with a second argument forces the result: on whenisOpenis true, off when false. Use this when you already know the state you want.el.classList.contains("open")returnstrueorfalsewithout changing anything.el.classList.replace("draft", "published")swaps one class for another in place.
How do you keep aria-expanded in step with the class?
A class only changes what sighted visitors see. Someone using a screen reader hears the button announced as "Menu, button, collapsed" or "expanded" based on its aria-expanded attribute. If the class says open and the attribute says false, those visitors get the wrong story.
The fix is to set both from one value. toggle returns the new state, so store it and use it for the attribute. Attributes are always strings, so convert the boolean with String(isOpen); passing the boolean directly also works because setAttribute converts it, but writing it out makes the intent clear when someone reads the code later.
You can also go the other way and treat the attribute as the source of truth, styling the menu with a selector like [aria-expanded="true"] + .menu. Either works. Here one value drives both, so they cannot drift apart. Try the demo with the Events lens open: each click shows the listener running, and the Changes lens outlines both the class and the attribute changing together.
button.addEventListener("click", () => {
const isOpen = menu.classList.toggle("open");
button.setAttribute("aria-expanded", String(isOpen));
});When is element.style the right choice?
The style property writes inline styles, the same as a style="" attribute in the HTML. Inline styles beat almost every rule in your stylesheet, which makes them handy for values the stylesheet cannot know in advance and a nuisance for everything else.
Good uses are one-off values computed at run time: the width of a progress bar from a percentage, the position of a tooltip under the pointer, the height of a panel measured from its content. These are numbers, not states, and writing a class for every possible percentage makes no sense.
Property names on style use camelCase, because a hyphen is a minus sign in JavaScript. background-color becomes backgroundColor, font-size becomes fontSize, and z-index becomes zIndex. Values are strings and need their units: "20px", not 20. A misspelt name such as fontsize throws no error; it creates an ordinary JavaScript property that the browser ignores, so check the result on screen.
To remove an inline style, set it to an empty string. The element then falls back to whatever the stylesheet says.
const bar = document.querySelector(".progress-bar");
const percent = 65;
bar.style.width = `${percent}%`;
bar.style.backgroundColor = "#2d6a4f";
// later: hand control back to the stylesheet
bar.style.backgroundColor = "";How do you set a CSS custom property from JavaScript?
Custom properties, the --name values you read with var(), are often the best bridge between a script and a stylesheet. The script supplies a number; the CSS decides where it goes. One custom property can feed a width, a colour mix and a transform at once, and the stylesheet stays readable.
Custom properties do not have camelCase names on style, so you cannot write style.--accent. Use style.setProperty("--accent", "#c0392b") to set one and style.removeProperty("--accent") to remove it. The two leading dashes are part of the name; leave them off and the browser treats it as an unknown property and ignores it.
Set the property on the element that needs it, or on document.documentElement (the <html> element) for a page-wide value such as a theme colour. Everything inside inherits it.
/* style.css */
.loaf { border-left: 6px solid var(--accent, #2d6a4f); }
// script.js
const card = document.querySelector(".loaf");
card.style.setProperty("--accent", "#c0392b");
document.documentElement.style.setProperty("--gap", "24px");How do you read the style an element really has?
Reading el.style.color only tells you about inline styles. If the colour comes from the stylesheet, el.style.color is an empty string, which surprises people the first time they try it.
Use getComputedStyle(el) to get the final values after every rule, inline style and inherited value has been applied. It returns an object you read with the same camelCase names, or with getPropertyValue("font-size") for hyphenated and custom property names. Colours come back in rgb() form and lengths in pixels, whatever units the CSS used.
Computed styles are read-only, and reading them forces the browser to work out layout, so avoid calling it hundreds of times in a loop. For checking state, prefer classList.contains: it is faster and says what you mean.
const title = document.querySelector("h1");
const styles = getComputedStyle(title);
console.log(title.style.fontSize); // "" (no inline style)
console.log(styles.fontSize); // e.g. "32px"
console.log(styles.getPropertyValue("--accent"));What goes wrong in AI-written code here?
Code from AI tools often sets a long run of inline styles to show or hide something: el.style.display, el.style.opacity, el.style.transform. It works on the first try and gets harder to change every week after. When you see three or more style lines that describe one state, move them into a class and toggle it.
Other things to look for: className = "open", which wipes out the element's other classes; class names written with a dot inside classList.add; aria-expanded set once and never updated; and checks like if (el.style.display === "none") that fail because the element was hidden by the stylesheet, which style cannot see. Replace that last one with classList.contains or the hidden property.
Also check that the CSS for the class exists. Toggling is-open does nothing if the stylesheet only has .open. The Changes lens shows the class arriving on the element; if nothing changes on screen, the stylesheet is the place to look.
How do you check it worked?
Run the page and click. The Changes lens outlines elements whose classes or attributes changed, so you can confirm that the class arrived on the right element. Open the Console and log menu.classList or menu.className before and after to see the exact list.
For accessibility, read the button's aria-expanded in the Console with button.getAttribute("aria-expanded") after each click. It should flip every time the menu does. In real browser DevTools, the Elements panel highlights changed attributes in colour for a moment, and the Styles panel shows which rule supplied each value, including inline ones marked as element.style.
Field notes
- Switch classes for known states, and keep the actual styles in your CSS.
classList.toggleadds a class if missing and removes it if present.- Set
aria-expandedso screen reader users hear whether the menu is open. - Use
element.stylefor one-off values, since inline styles override most rules.
Easy to get wrong
Assigning className replaces every class, so the element loses menu and any styles that depended on it. classList.add changes only the one class.
Class names on an element have no dot. The dot is selector syntax, and with it you add a class nothing in your CSS matches.
A hyphen is subtraction in JavaScript, so this line is a syntax error or a calculation. Style properties use camelCase names.
Hard-coding "true" means the attribute is wrong every other click. Drive both from the value toggle returns.
Questions people ask
Should I use classList or setAttribute("class", …)?
Use classList for changing one class. setAttribute("class", …) replaces the whole list, the same as className, and is only useful when you really want to set every class at once.
Why is el.style.display empty when the element is clearly hidden?
The style property only shows inline styles. A value from the stylesheet appears in getComputedStyle(el).display instead.
Can I animate a class change?
Yes. Put a transition on the base rule in CSS, and the change triggered by adding or removing a class animates on its own. The script needs no timing code.
Is there a limit on how many classes an element can have?
No practical limit. Keep names that describe state, such as open, sold-out or has-error, so the script and the stylesheet read the same way.
What about the hidden property?
For plain show and hide, el.hidden = true is simpler than a class and is understood by every browser. Use a class when the two states differ in more than visibility.