Style and Class Manipulation: Dynamically Controlling Element Appearance
Two Approaches to Style Manipulation
There are two main ways to control element styles in JavaScript: manipulating CSS classes and directly modifying inline styles.
Manipulating CSS classes follows the "separation of concerns" principle—styles are defined in CSS, and JavaScript only adds or removes class names. This approach is easier to maintain and makes it easier to implement complex style changes.
Directly modifying styles is suitable for scenarios that need dynamic calculation, such as setting element coordinates based on mouse position, or implementing simple immediate effects.
Both approaches have their pros and cons, and they're often combined in actual development.
classList: Class Name Manipulation
classList is a modern API for manipulating element class names, providing intuitive methods to add, remove, and toggle class names.
Basic Operations
const element = document.querySelector(".box");
// Add class
element.classList.add("active");
element.classList.add("highlight", "visible"); // Add multiple at once
// Remove class
element.classList.remove("hidden");
element.classList.remove("old", "deprecated"); // Remove multiple at once
// Toggle class (remove if exists, add if doesn't)
element.classList.toggle("expanded");
// Force add or remove
element.classList.toggle("visible", true); // Force add
element.classList.toggle("visible", false); // Force remove
// Check if contains a class
if (element.classList.contains("active")) {
console.log("Element is active");
}
// Replace class
element.classList.replace("old-class", "new-class");Iterating Class Names
const element = document.querySelector(".multi-class");
// Get class name count
console.log(element.classList.length);
// Access by index
console.log(element.classList[0]);
console.log(element.classList.item(0)); // Same effect
// Iterate through all class names
for (const className of element.classList) {
console.log(className);
}
// Convert to array
const classArray = [...element.classList];
const classArray2 = Array.from(element.classList);className Property
Before classList, className was used to manipulate class names:
const element = document.querySelector(".box");
// Get complete class name string
console.log(element.className); // "box active highlight"
// Set class names (will overwrite all original classes)
element.className = "new-class another-class";
// Append class name (need to manually handle spaces)
element.className += " extra-class";The problem with className is that it returns and accepts a complete class name string. Adding or removing individual classes requires string manipulation, which is error-prone. In modern development, classList should be preferred.
Practical Application: Theme Switching
class ThemeManager {
constructor() {
this.body = document.body;
this.themes = ["light", "dark", "sepia"];
this.currentIndex = 0;
// Restore theme from localStorage
const savedTheme = localStorage.getItem("theme");
if (savedTheme && this.themes.includes(savedTheme)) {
this.currentIndex = this.themes.indexOf(savedTheme);
}
this.applyTheme();
}
applyTheme() {
// Remove all theme classes
this.themes.forEach((theme) => {
this.body.classList.remove(`theme-${theme}`);
});
// Add current theme class
const currentTheme = this.themes[this.currentIndex];
this.body.classList.add(`theme-${currentTheme}`);
// Save to localStorage
localStorage.setItem("theme", currentTheme);
}
setTheme(themeName) {
const index = this.themes.indexOf(themeName);
if (index !== -1) {
this.currentIndex = index;
this.applyTheme();
}
}
cycleTheme() {
this.currentIndex = (this.currentIndex + 1) % this.themes.length;
this.applyTheme();
}
getCurrentTheme() {
return this.themes[this.currentIndex];
}
}
// Usage
const themeManager = new ThemeManager();
document.querySelector("#theme-toggle").addEventListener("click", () => {
themeManager.cycleTheme();
});style Property: Inline Styles
Every element has a style property for reading and writing inline styles.
Basic Operations
const element = document.querySelector(".box");
// Set individual styles
element.style.color = "red";
element.style.backgroundColor = "white";
element.style.fontSize = "16px";
element.style.marginTop = "20px";
// Read styles
console.log(element.style.color); // "red"
// Clear style (set to empty string)
element.style.color = "";Property Name Conversion
CSS property names use hyphens (like background-color), while JavaScript uses camelCase (like backgroundColor):
| CSS Property | JavaScript Property |
|---|---|
| background-color | backgroundColor |
| font-size | fontSize |
| border-radius | borderRadius |
| margin-top | marginTop |
| z-index | zIndex |
| -webkit-transform | webkitTransform |
// CSS: background-color → JS: backgroundColor
element.style.backgroundColor = "blue";
// CSS: border-top-left-radius → JS: borderTopLeftRadius
element.style.borderTopLeftRadius = "10px";
// Properties with vendor prefixes
element.style.webkitTransform = "rotate(45deg)";
element.style.MozTransform = "rotate(45deg)"; // Note Moz capitalizationcssText: Batch Set Styles
Using cssText allows setting multiple styles at once:
const element = document.querySelector(".box");
// Set multiple styles (will overwrite existing style attribute)
element.style.cssText = "color: red; background: white; font-size: 16px;";
// Append styles
element.style.cssText += "padding: 10px; margin: 20px;";Note that cssText uses CSS syntax (kebab-case) and overwrites all previous inline styles.
Using setProperty and getPropertyValue
These methods are standardized interfaces provided by the style object:
const element = document.querySelector(".box");
// Set style (using CSS property names)
element.style.setProperty("background-color", "blue");
element.style.setProperty("--custom-color", "#ff0000"); // CSS variable
// Get style
console.log(element.style.getPropertyValue("background-color"));
// Remove style
element.style.removeProperty("background-color");
// Set priority
element.style.setProperty("color", "red", "important");Limitations of style Property
element.style can only read and set inline styles, cannot get styles defined through CSS files or <style> tags:
<style>
.box {
color: blue;
}
</style>
<div class="box" style="background: white;">Content</div>const box = document.querySelector(".box");
console.log(box.style.color); // "" (empty string, not "blue")
console.log(box.style.background); // "white" (inline styles can be read)getComputedStyle: Get Computed Styles
To get the final rendered styles of an element (including inherited styles, results after CSS rules are applied), you need to use getComputedStyle:
const element = document.querySelector(".box");
const styles = getComputedStyle(element);
// Read styles (using CSS property names or camelCase)
console.log(styles.color); // "rgb(255, 0, 0)"
console.log(styles.getPropertyValue("color")); // "rgb(255, 0, 0)"
console.log(styles.fontSize); // "16px"
console.log(styles.backgroundColor); // "rgb(255, 255, 255)"A few points to note:
- Returns a read-only object, cannot be used to set styles
- Values are computed results, units are converted (like
emtopx) - Colors are converted to rgb/rgba format
- Shorthand properties may not work, need to use specific properties
const styles = getComputedStyle(element);
// ❌ Shorthand property may not work
console.log(styles.margin); // May be empty string or separated values
// ✅ Use specific properties
console.log(styles.marginTop);
console.log(styles.marginRight);
console.log(styles.marginBottom);
console.log(styles.marginLeft);Getting Pseudo-element Styles
getComputedStyle can get pseudo-element styles:
const element = document.querySelector(".box");
// Get ::before pseudo-element styles
const beforeStyles = getComputedStyle(element, "::before");
console.log(beforeStyles.content);
// Get ::after pseudo-element styles
const afterStyles = getComputedStyle(element, "::after");
console.log(afterStyles.color);Calculate Actual Dimensions
Combine getComputedStyle and element geometric properties to get accurate dimension information:
const element = document.querySelector(".box");
const styles = getComputedStyle(element);
// Method 1: Use getComputedStyle
const width = parseFloat(styles.width);
const height = parseFloat(styles.height);
const paddingLeft = parseFloat(styles.paddingLeft);
const paddingRight = parseFloat(styles.paddingRight);
console.log(`Content width: ${width}px`);
console.log(`Padding: ${paddingLeft}px + ${paddingRight}px`);
// Method 2: Use element properties (usually more convenient)
console.log(element.offsetWidth); // Includes border and padding
console.log(element.clientWidth); // Includes padding, no border
console.log(element.scrollWidth); // Includes overflow content
// getBoundingClientRect provides most complete dimension information
const rect = element.getBoundingClientRect();
console.log(rect.width); // Element width (including border)
console.log(rect.height); // Element height (including border)
console.log(rect.top); // Distance from viewport top
console.log(rect.left); // Distance from viewport leftCSS Variable Manipulation
CSS custom properties (CSS variables) can be dynamically modified through JavaScript:
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
--font-size-base: 16px;
--spacing-unit: 8px;
}
.box {
background-color: var(--primary-color);
font-size: var(--font-size-base);
padding: calc(var(--spacing-unit) * 2);
}// Get root element CSS variables
const root = document.documentElement;
const styles = getComputedStyle(root);
const primaryColor = styles.getPropertyValue("--primary-color").trim();
console.log(primaryColor); // "#007bff"
// Set CSS variable
root.style.setProperty("--primary-color", "#ff0000");
// Set on specific element (will override inherited value)
const element = document.querySelector(".box");
element.style.setProperty("--primary-color", "#00ff00");
// Remove CSS variable
root.style.removeProperty("--primary-color");Practical Application: Dynamic Theme Colors
class ColorSchemeManager {
constructor() {
this.root = document.documentElement;
this.defaultScheme = {
"--primary-color": "#007bff",
"--primary-light": "#4dabf7",
"--primary-dark": "#0056b3",
"--background": "#ffffff",
"--surface": "#f8f9fa",
"--text-primary": "#212529",
"--text-secondary": "#6c757d",
};
}
setScheme(scheme) {
Object.entries(scheme).forEach(([property, value]) => {
this.root.style.setProperty(property, value);
});
}
setColor(property, value) {
this.root.style.setProperty(property, value);
}
getColor(property) {
return getComputedStyle(this.root).getPropertyValue(property).trim();
}
reset() {
this.setScheme(this.defaultScheme);
}
// Generate color scheme from user-selected color
generateFromPrimary(hexColor) {
const rgb = this.hexToRgb(hexColor);
this.setScheme({
"--primary-color": hexColor,
"--primary-light": this.lighten(rgb, 20),
"--primary-dark": this.darken(rgb, 20),
...this.defaultScheme,
});
}
hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
}
rgbToHex({ r, g, b }) {
return (
"#" +
[r, g, b]
.map((x) => {
const hex = Math.round(x).toString(16);
return hex.length === 1 ? "0" + hex : hex;
})
.join("")
);
}
lighten({ r, g, b }, percent) {
return this.rgbToHex({
r: Math.min(255, r + (255 - r) * (percent / 100)),
g: Math.min(255, g + (255 - g) * (percent / 100)),
b: Math.min(255, b + (255 - b) * (percent / 100)),
});
}
darken({ r, g, b }, percent) {
return this.rgbToHex({
r: Math.max(0, r * (1 - percent / 100)),
g: Math.max(0, g * (1 - percent / 100)),
b: Math.max(0, b * (1 - percent / 100)),
});
}
}
// Usage
const colorManager = new ColorSchemeManager();
document.querySelector("#color-picker").addEventListener("input", (e) => {
colorManager.generateFromPrimary(e.target.value);
});Animation and Transitions
CSS animations and transitions can be triggered through JavaScript.
Trigger Transitions
const element = document.querySelector(".box");
// Define transition in CSS
// .box { transition: transform 0.3s ease, opacity 0.3s ease; }
// JavaScript changing styles triggers transition
element.style.transform = "translateX(100px)";
element.style.opacity = "0.5";
// Listen for transition end
element.addEventListener("transitionend", (e) => {
console.log(`Transition ended: ${e.propertyName}`);
});Force Trigger Animation
Sometimes you need to retrigger an animation, which can be done by removing and re-adding a class:
function replayAnimation(element, animationClass) {
element.classList.remove(animationClass);
// Force reflow to restart animation
void element.offsetWidth; // Reading offsetWidth triggers reflow
element.classList.add(animationClass);
}
// Usage
const button = document.querySelector(".animated-button");
button.addEventListener("click", () => {
replayAnimation(button, "shake");
});Using Web Animations API
Modern browsers support the more powerful Web Animations API:
const element = document.querySelector(".box");
// Create animation
const animation = element.animate(
[
{ transform: "translateY(0)", opacity: 1 },
{ transform: "translateY(-50px)", opacity: 0.5, offset: 0.5 },
{ transform: "translateY(0)", opacity: 1 },
],
{
duration: 1000,
easing: "ease-in-out",
iterations: Infinity,
}
);
// Control animation
animation.pause();
animation.play();
animation.reverse();
animation.cancel();
// Adjust playback rate
animation.playbackRate = 2; // Double speed
// Listen to events
animation.onfinish = () => console.log("Animation finished");Practical Application Examples
Interactive Card Effects
class InteractiveCard {
constructor(element) {
this.card = element;
this.shine = this.createShineElement();
this.card.appendChild(this.shine);
this.bindEvents();
}
createShineElement() {
const shine = document.createElement("div");
shine.className = "card-shine";
shine.style.cssText = `
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
135deg,
rgba(255,255,255,0) 0%,
rgba(255,255,255,0.1) 50%,
rgba(255,255,255,0) 100%
);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
`;
return shine;
}
bindEvents() {
this.card.addEventListener("mouseenter", () => this.onEnter());
this.card.addEventListener("mouseleave", () => this.onLeave());
this.card.addEventListener("mousemove", (e) => this.onMove(e));
}
onEnter() {
this.shine.style.opacity = "1";
this.card.style.transition = "transform 0.1s ease-out";
}
onLeave() {
this.shine.style.opacity = "0";
this.card.style.transform = "perspective(1000px) rotateX(0) rotateY(0)";
this.card.style.transition = "transform 0.5s ease-out";
}
onMove(e) {
const rect = this.card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const rotateX = (y - centerY) / 10;
const rotateY = (centerX - x) / 10;
this.card.style.transform = `
perspective(1000px)
rotateX(${rotateX}deg)
rotateY(${rotateY}deg)
scale3d(1.02, 1.02, 1.02)
`;
// Update shine position
const percentX = (x / rect.width) * 100;
const percentY = (y / rect.height) * 100;
this.shine.style.background = `
radial-gradient(
circle at ${percentX}% ${percentY}%,
rgba(255,255,255,0.3) 0%,
rgba(255,255,255,0) 60%
)
`;
}
}
// Usage
document.querySelectorAll(".interactive-card").forEach((card) => {
new InteractiveCard(card);
});Scroll Progress Indicator
class ScrollProgress {
constructor() {
this.progressBar = this.createProgressBar();
document.body.prepend(this.progressBar);
window.addEventListener("scroll", () => this.updateProgress());
window.addEventListener("resize", () => this.updateProgress());
this.updateProgress();
}
createProgressBar() {
const bar = document.createElement("div");
bar.className = "scroll-progress";
bar.style.cssText = `
position: fixed;
top: 0;
left: 0;
height: 4px;
background: linear-gradient(90deg, #007bff, #00bcd4);
z-index: 9999;
transition: width 0.1s ease-out;
box-shadow: 0 0 10px rgba(0, 123, 255, 0.5);
`;
return bar;
}
updateProgress() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const docHeight =
document.documentElement.scrollHeight - window.innerHeight;
const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
this.progressBar.style.width = `${progress}%`;
}
}
// Usage
new ScrollProgress();Responsive Style Manager
class ResponsiveStyleManager {
constructor() {
this.breakpoints = {
mobile: 576,
tablet: 768,
desktop: 992,
wide: 1200,
};
this.currentBreakpoint = null;
this.callbacks = [];
this.check();
window.addEventListener("resize", () => this.check());
}
check() {
const width = window.innerWidth;
let newBreakpoint;
if (width < this.breakpoints.mobile) {
newBreakpoint = "xs";
} else if (width < this.breakpoints.tablet) {
newBreakpoint = "mobile";
} else if (width < this.breakpoints.desktop) {
newBreakpoint = "tablet";
} else if (width < this.breakpoints.wide) {
newBreakpoint = "desktop";
} else {
newBreakpoint = "wide";
}
if (newBreakpoint !== this.currentBreakpoint) {
const oldBreakpoint = this.currentBreakpoint;
this.currentBreakpoint = newBreakpoint;
// Update body classes
document.body.classList.remove(
"bp-xs",
"bp-mobile",
"bp-tablet",
"bp-desktop",
"bp-wide"
);
document.body.classList.add(`bp-${newBreakpoint}`);
// Update CSS variables
document.documentElement.style.setProperty(
"--current-breakpoint",
`"${newBreakpoint}"`
);
// Trigger callbacks
this.callbacks.forEach((cb) => cb(newBreakpoint, oldBreakpoint));
}
}
onChange(callback) {
this.callbacks.push(callback);
// Call immediately
callback(this.currentBreakpoint, null);
// Return unsubscribe function
return () => {
const index = this.callbacks.indexOf(callback);
if (index > -1) this.callbacks.splice(index, 1);
};
}
is(breakpoint) {
return this.currentBreakpoint === breakpoint;
}
isAtLeast(breakpoint) {
const order = ["xs", "mobile", "tablet", "desktop", "wide"];
return order.indexOf(this.currentBreakpoint) >= order.indexOf(breakpoint);
}
}
// Usage
const responsive = new ResponsiveStyleManager();
responsive.onChange((current, previous) => {
console.log(`Breakpoint changed from ${previous} to ${current}`);
if (current === "mobile" || current === "xs") {
// Mobile-specific logic
document.querySelector(".sidebar")?.classList.add("collapsed");
} else {
document.querySelector(".sidebar")?.classList.remove("collapsed");
}
});Style Validator
class StyleValidator {
constructor(element) {
this.element = element;
this.rules = [];
}
addRule(property, validator, message) {
this.rules.push({ property, validator, message });
return this;
}
validate() {
const styles = getComputedStyle(this.element);
const errors = [];
this.rules.forEach((rule) => {
const value = styles.getPropertyValue(rule.property);
if (!rule.validator(value)) {
errors.push({
property: rule.property,
value,
message: rule.message,
});
}
});
return {
valid: errors.length === 0,
errors,
};
}
// Preset validation rules
static minFontSize(pixels) {
return (value) => parseFloat(value) >= pixels;
}
static hasColor(value) {
return value && value !== "rgba(0, 0, 0, 0)" && value !== "transparent";
}
static minContrast(background, foreground, ratio = 4.5) {
// Simplified contrast check
return true; // Actual implementation needs WCAG contrast calculation
}
}
// Usage
const validator = new StyleValidator(document.querySelector(".content"));
validator
.addRule(
"font-size",
StyleValidator.minFontSize(14),
"Font size should be at least 14px"
)
.addRule("color", StyleValidator.hasColor, "Text color should be visible");
const result = validator.validate();
if (!result.valid) {
console.warn("Style validation failed:", result.errors);
}Performance Optimization Recommendations
Avoid Frequent Style Reads/Writes
// ❌ Bad: Alternating reads/writes triggers multiple reflows
elements.forEach((el) => {
const height = el.offsetHeight; // Read
el.style.height = height * 2 + "px"; // Write
});
// ✅ Good: Batch read first, then batch write
const heights = elements.map((el) => el.offsetHeight); // Batch read
elements.forEach((el, i) => {
el.style.height = heights[i] * 2 + "px"; // Batch write
});Use CSS Classes Instead of Inline Styles
// ❌ Slower: Multiple modifications to inline styles
element.style.color = "red";
element.style.fontSize = "20px";
element.style.fontWeight = "bold";
element.style.marginTop = "10px";
// ✅ Faster: Add predefined CSS class
element.classList.add("highlighted");Use transform and opacity for Animation
// ❌ Triggers layout reflow
element.style.left = "100px";
element.style.top = "50px";
element.style.width = "200px";
// ✅ Only triggers compositing, better performance
element.style.transform = "translate(100px, 50px) scale(2)";
element.style.opacity = "0.8";Summary
Style and class manipulation are important ways for JavaScript to interact with CSS. Choosing the right methods makes code clearer and more performant:
| Need | Recommended Method |
|---|---|
| Add/remove/toggle classes | classList.add/remove/toggle |
| Check if has class | classList.contains |
| Set individual style | element.style.property |
| Batch set styles | element.style.cssText |
| Get computed styles | getComputedStyle |
| Manipulate CSS variables | setProperty/getPropertyValue |
| Get element dimensions | getBoundingClientRect |
Best practices:
- Prioritize CSS classes, keep style definitions in CSS
- Use
getComputedStyleto read final rendered styles - Batch read/write styles to avoid triggering frequent reflows
- Prioritize
transformandopacityfor animations