Skip to content

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

javascript
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

javascript
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:

javascript
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

javascript
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

javascript
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 PropertyJavaScript Property
background-colorbackgroundColor
font-sizefontSize
border-radiusborderRadius
margin-topmarginTop
z-indexzIndex
-webkit-transformwebkitTransform
javascript
// 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 capitalization

cssText: Batch Set Styles

Using cssText allows setting multiple styles at once:

javascript
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:

javascript
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:

html
<style>
  .box {
    color: blue;
  }
</style>
<div class="box" style="background: white;">Content</div>
javascript
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:

javascript
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:

  1. Returns a read-only object, cannot be used to set styles
  2. Values are computed results, units are converted (like em to px)
  3. Colors are converted to rgb/rgba format
  4. Shorthand properties may not work, need to use specific properties
javascript
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:

javascript
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:

javascript
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 left

CSS Variable Manipulation

CSS custom properties (CSS variables) can be dynamically modified through JavaScript:

css
: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);
}
javascript
// 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

javascript
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

javascript
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:

javascript
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:

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
// ❌ 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

javascript
// ❌ 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

javascript
// ❌ 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:

NeedRecommended Method
Add/remove/toggle classesclassList.add/remove/toggle
Check if has classclassList.contains
Set individual styleelement.style.property
Batch set styleselement.style.cssText
Get computed stylesgetComputedStyle
Manipulate CSS variablessetProperty/getPropertyValue
Get element dimensionsgetBoundingClientRect

Best practices:

  1. Prioritize CSS classes, keep style definitions in CSS
  2. Use getComputedStyle to read final rendered styles
  3. Batch read/write styles to avoid triggering frequent reflows
  4. Prioritize transform and opacity for animations