Accessing CSS from JavaScript
|
This section documents modern ECMAScript and core browser JavaScript APIs — it is not tied to any specific framework or library (React, Vue, Angular, etc.). This content was generated with the assistance of AI and should be verified against the current ECMAScript specification and MDN documentation before relying on it in production, since JavaScript language features and browser API support continue to evolve. This section’s bibliography lists the reference material consulted while preparing these pages. |
Beyond querying and mutating the document tree (see The DOM), JavaScript can also control how elements look: toggling CSS classes, reading and writing inline styles, asking the browser what style actually ended up applying to an element, and reacting to the lifecycle events fired by CSS transitions and animations. This page covers those four techniques — for how the CSS itself is authored, see Animations & Keyframes and Transitions.
The classList API
The simplest and most robust way to script an element’s appearance is to add and remove CSS class names rather
than individual style properties, letting a stylesheet decide what each class actually looks like. Every
Element exposes its class attribute as a set-like object through the classList property (class itself is
a reserved word in JavaScript, so the raw attribute is mirrored as className — a single space-separated
string — while classList treats it as a collection of individual names):
let spinner = document.querySelector("#spinner");
spinner.classList.add("animated"); // add a class (no-op if already present)
spinner.classList.remove("hidden"); // remove a class (no-op if absent)
spinner.classList.contains("hidden"); // => false -- test membership
spinner.classList.toggle("animated"); // remove if present, add if absent
spinner.classList.toggle("animated", isBusy); // force-add when isBusy is truthy, force-remove otherwise
Given a stylesheet rule like .hidden { display: none; }, hiding and showing an element becomes a matter of
toggling that one class rather than juggling raw display values:
document.querySelector("#tooltip").classList.remove("hidden"); // show it
document.querySelector("#tooltip").classList.add("hidden"); // hide it again
Prefer classList over setting individual style properties whenever the set of possible appearances is known
ahead of time and can be expressed as stylesheet classes — it keeps presentation in CSS and keeps the JavaScript
declarative ("this element is now `.active`") rather than imperative ("set these six properties").
Inline Styles
Sometimes an element’s style genuinely can’t be expressed as a fixed set of classes — positioning a tooltip at
an arbitrary (x, y) computed at runtime is the classic example. In that case, script the style attribute
directly. Every Element has a style property that mirrors its style attribute, but unlike most such
properties it is not a string: it is a CSSStyleDeclaration object, a parsed, live view of the inline styles.
function displayAt(tooltip, x, y) {
tooltip.style.display = "block";
tooltip.style.position = "absolute";
tooltip.style.left = `${x}px`;
tooltip.style.top = `${y}px`;
}
Two things trip people up the first time they script style:
-
Property names lose their hyphens. A CSS property name written with hyphens (
font-size,border-left-width,background-color) becomes a camelCased JavaScript property (fontSize,borderLeftWidth,backgroundColor), because a hyphen would otherwise be parsed as a minus sign. -
Values are always strings, and units are never implied.
element.style.marginLeft = 300is silently wrong (a number, not a string);element.style.marginLeft = "300"is also wrong (no unit). The correct form always includes the unit:element.style.marginLeft = "300px".
element.style.display = "block";
element.style.fontFamily = "sans-serif";
element.style.backgroundColor = "#ffffff";
element.style.marginLeft = "300px"; // unit required
element.style.left = `${x0 + leftBorder + leftPadding}px`; // append the unit after computing
Shorthand CSS properties (margin, border, font, …) still work as a single JavaScript property covering
all their longhand parts:
element.style.margin = `${top}px ${right}px ${bottom}px ${left}px`;
To read or write every inline style at once as a single string — rather than property by property — use either
getAttribute("style") / setAttribute("style", …) or the cssText property of the CSSStyleDeclaration
object, which are equivalent:
target.setAttribute("style", source.getAttribute("style")); // copy inline styles from source to target
target.style.cssText = source.style.cssText; // same effect
Keep in mind that element.style reflects only the inline style attribute — it says nothing about styles
that apply to the element from a stylesheet, which is the overwhelming majority of real-world styling. Querying
element.style to find out how an element is actually being rendered almost always gives the wrong answer;
getComputedStyle(), covered next, is what answers that question.
Computed Styles
The computed style of an element is what the browser actually used to render it — the result of combining the
element’s inline style with every applicable stylesheet rule, with the cascade and specificity already resolved.
Obtain it with window.getComputedStyle(), passing the element and, optionally, a pseudo-element string such as
"::before":
let title = document.querySelector("#section1title");
let styles = window.getComputedStyle(title);
let beforeStyles = window.getComputedStyle(title, "::before");
styles.fontSize; // e.g. "16px" -- always resolved to an absolute pixel value
styles.color; // e.g. "rgb(51, 51, 51)"
A computed CSSStyleDeclaration differs from an inline one in several important ways:
-
It is read-only. Assigning to a property of a computed style object has no effect and (in strict mode) throws.
-
Values are absolute. Percentages and other relative units are resolved to pixels, so
styles.marginTopis always something like"12px", never"1em"or"5%"— still a string you must parse, but without needing to handle every possible unit. -
Shorthand properties are not computed. Query
marginLeft,marginTop,marginRight, andmarginBottomindividually rather thanmargin; queryborderLeftWidthrather thanborderorborderWidth. -
cssTextis undefined on a computed style.
Computed styles can still be surprising: querying fontFamily returns whatever comma-separated font list was
declared ("arial, helvetica, sans-serif"), not the typeface actually rendered, and querying top/left on an
element that isn’t absolutely positioned typically returns the literal string "auto". For an element’s actual
on-screen size and position, prefer getBoundingClientRect() (see
Document Geometry & Scrolling) over parsing computed style
values.
Reacting to Animation and Transition Events
CSS transitions and animations (authored as described in Transitions and Animations & Keyframes) run entirely inside the browser’s rendering engine — JavaScript does not have to drive a single frame of them. What JavaScript can do is trigger an animation (typically just by adding or removing a class) and then listen for the lifecycle events the browser fires as that animation progresses.
Transition Events
A CSS transition fires three events, in order, at the element being animated:
| Event | Fired when |
|---|---|
|
The transition is triggered — may fire before any visible change, if |
|
The visual change actually begins (after any delay has elapsed). |
|
The transition finishes. |
Handlers receive a TransitionEvent, whose propertyName identifies which CSS property was animated and whose
elapsedTime (on transitionend) reports how many seconds elapsed since transitionstart:
let panel = document.querySelector("#subscribe");
panel.addEventListener("transitionend", (event) => {
console.log(`${event.propertyName} finished animating after ${event.elapsedTime}s`);
panel.classList.remove("fading"); // e.g. clean up a helper class once the fade completes
});
// Given `.fadeable { transition: opacity .5s ease-in; }` in the stylesheet,
// this single class toggle is enough to trigger the animated fade:
panel.classList.add("transparent");
Animation Events
CSS @keyframes animations similarly fire events at the animated element — animationstart when the animation
begins, animationend when it completes, and animationiteration after each repetition except the last (for an
animation whose animation-iteration-count is greater than one). Handlers receive an AnimationEvent, whose
animationName identifies the @keyframes name in play and whose elapsedTime reports seconds elapsed since
the animation started:
let banner = document.querySelector("#banner");
banner.addEventListener("animationstart", (event) => {
console.log(`animation "${event.animationName}" started`);
});
banner.addEventListener("animationend", (event) => {
banner.classList.remove("attention"); // remove the triggering class once it's done
});
banner.classList.add("attention"); // triggers the animation defined for that class
As with transitions, JavaScript’s role is limited to starting the animation (usually via classList) and
reacting to its lifecycle — the interpolation itself is handled entirely by the browser.