How to Use classList toggle in JavaScript DOM

Master the classList.toggle() method in JavaScript. Learn how to toggle CSS classes on and off with the force parameter, return values, and real-world interactive UI patterns.

JavaScriptbeginner
8 min read

The classList.toggle() method is one of the most useful DOM tools for building interactive UIs. It adds a class if it is missing and removes it if it is present, all in a single call. Combined with CSS transitions, it powers dark mode switches, expandable menus, accordion panels, modal dialogs, and virtually every show/hide interaction on the web. This guide covers everything from basic syntax to advanced patterns.

Basic Syntax

javascriptjavascript
element.classList.toggle(className);
element.classList.toggle(className, force);

The method takes one required parameter (the class name) and one optional parameter (a boolean force):

javascriptjavascript
const box = document.getElementById("box");
 
// If "active" is absent, add it. If present, remove it.
box.classList.toggle("active");

Return Value

The toggle method returns a boolean: true if the class was added, false if it was removed.

javascriptjavascript
const element = document.querySelector(".panel");
 
const wasAdded = element.classList.toggle("expanded");
 
if (wasAdded) {
  console.log("Panel is now expanded");
} else {
  console.log("Panel is now collapsed");
}

This return value is incredibly useful for updating other parts of the UI:

javascriptjavascript
const menuButton = document.getElementById("menu-toggle");
const nav = document.querySelector(".mobile-nav");
 
menuButton.addEventListener("click", () => {
  const isOpen = nav.classList.toggle("open");
 
  // Update button text based on state
  menuButton.textContent = isOpen ? "Close Menu" : "Open Menu";
 
  // Update ARIA attribute for accessibility
  menuButton.setAttribute("aria-expanded", isOpen);
 
  // Toggle body scroll
  document.body.classList.toggle("no-scroll", isOpen);
});

The Force Parameter

The second parameter forces the toggle to behave as either add (when true) or remove (when false):

javascriptjavascript
const element = document.querySelector(".card");
 
// Force add: always adds the class (like classList.add)
element.classList.toggle("visible", true);
 
// Force remove: always removes the class (like classList.remove)
element.classList.toggle("visible", false);

Why Use Force?

The force parameter lets you set a class based on a condition without if/else blocks:

javascriptjavascript
// WITHOUT force: verbose
function updateActiveState(element, isActive) {
  if (isActive) {
    element.classList.add("active");
  } else {
    element.classList.remove("active");
  }
}
 
// WITH force: clean one-liner
function updateActiveState(element, isActive) {
  element.classList.toggle("active", isActive);
}

Real-world uses of the force parameter:

javascriptjavascript
// Validate an input and show/hide error state
function validateEmail(input) {
  const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.value);
 
  input.classList.toggle("input-error", !isValid);
  input.classList.toggle("input-valid", isValid);
 
  return isValid;
}
 
// Enable/disable a submit button based on form state
function updateSubmitButton(form) {
  const allValid = form.checkValidity();
  const submitBtn = form.querySelector('button[type="submit"]');
 
  submitBtn.classList.toggle("btn-disabled", !allValid);
  submitBtn.disabled = !allValid;
}
 
// Responsive class based on scroll position
window.addEventListener("scroll", () => {
  const header = document.querySelector("header");
  header.classList.toggle("scrolled", window.scrollY > 50);
});

Toggle vs Add/Remove Comparison

Scenariotoggle()add() / remove()
Switch between on/off statestoggle("active")Requires if/else check
Always set to ontoggle("active", true)add("active")
Always set to offtoggle("active", false)remove("active")
Set based on conditiontoggle("active", condition)if/else with add/remove
Need the resulting stateReturns booleanMust call contains() after

Practical UI Patterns

Pattern 1: Dark Mode Toggle

javascriptjavascript
function initDarkMode() {
  const toggle = document.getElementById("dark-mode-toggle");
  const root = document.documentElement;
 
  // Restore saved preference
  const savedMode = localStorage.getItem("darkMode");
  if (savedMode === "true") {
    root.classList.add("dark");
    toggle.checked = true;
  }
 
  toggle.addEventListener("change", () => {
    const isDark = root.classList.toggle("dark");
    localStorage.setItem("darkMode", isDark);
 
    // Update toggle label
    document.getElementById("mode-label").textContent =
      isDark ? "Dark Mode" : "Light Mode";
  });
}
csscss
:root {
  --bg: #ffffff;
  --text: #1a1a2e;
  --card-bg: #f8f9fa;
}
 
:root.dark {
  --bg: #1a1a2e;
  --text: #e0e0e0;
  --card-bg: #16213e;
}
 
body {
  background: var(--bg);
  color: var(--text);
  transition: background-color 0.3s, color 0.3s;
}

Pattern 2: Accordion / FAQ Sections

javascriptjavascript
function initAccordion(containerId) {
  const container = document.getElementById(containerId);
 
  container.addEventListener("click", (event) => {
    const header = event.target.closest(".accordion-header");
    if (!header) return;
 
    const item = header.parentElement;
    const isExpanding = item.classList.toggle("expanded");
 
    // Update ARIA
    header.setAttribute("aria-expanded", isExpanding);
 
    // Optional: close other items for single-open behavior
    if (isExpanding) {
      const siblings = container.querySelectorAll(".accordion-item.expanded");
      siblings.forEach(sibling => {
        if (sibling !== item) {
          sibling.classList.remove("expanded");
          sibling.querySelector(".accordion-header")
            .setAttribute("aria-expanded", "false");
        }
      });
    }
  });
}
csscss
.accordion-content {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s ease;
}
 
.accordion-item.expanded .accordion-content {
  max-height: 500px;
}
 
.accordion-header::after {
  content: "+";
  transition: transform 0.3s;
}
 
.accordion-item.expanded .accordion-header::after {
  transform: rotate(45deg);
}

Pattern 3: Dropdown Menu

javascriptjavascript
function initDropdown(triggerId, menuId) {
  const trigger = document.getElementById(triggerId);
  const menu = document.getElementById(menuId);
 
  trigger.addEventListener("click", (event) => {
    event.stopPropagation();
    const isOpen = menu.classList.toggle("open");
    trigger.setAttribute("aria-expanded", isOpen);
  });
 
  // Close when clicking outside
  document.addEventListener("click", (event) => {
    if (!menu.contains(event.target) && !trigger.contains(event.target)) {
      menu.classList.remove("open");
      trigger.setAttribute("aria-expanded", "false");
    }
  });
 
  // Close on Escape key
  document.addEventListener("keydown", (event) => {
    if (event.key === "Escape" && menu.classList.contains("open")) {
      menu.classList.remove("open");
      trigger.setAttribute("aria-expanded", "false");
      trigger.focus();
    }
  });
}

Pattern 4: Scroll-Based Header

javascriptjavascript
function initStickyHeader() {
  const header = document.querySelector("header");
  let lastScroll = 0;
 
  window.addEventListener("scroll", () => {
    const currentScroll = window.scrollY;
 
    // Add "scrolled" class when past threshold
    header.classList.toggle("scrolled", currentScroll > 100);
 
    // Hide header on scroll down, show on scroll up
    header.classList.toggle("hidden", currentScroll > lastScroll && currentScroll > 200);
 
    lastScroll = currentScroll;
  });
}
csscss
header {
  position: fixed;
  top: 0;
  width: 100%;
  background: transparent;
  transition: all 0.3s ease;
  transform: translateY(0);
}
 
header.scrolled {
  background: white;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
 
header.hidden {
  transform: translateY(-100%);
}

Pattern 5: Tab Navigation

javascriptjavascript
function initTabs(containerSelector) {
  const container = document.querySelector(containerSelector);
  const tabs = container.querySelectorAll("[role='tab']");
  const panels = container.querySelectorAll("[role='tabpanel']");
 
  tabs.forEach(tab => {
    tab.addEventListener("click", () => {
      const targetPanel = tab.getAttribute("aria-controls");
 
      // Toggle active states
      tabs.forEach(t => {
        const isActive = t === tab;
        t.classList.toggle("active", isActive);
        t.setAttribute("aria-selected", isActive);
      });
 
      panels.forEach(panel => {
        panel.classList.toggle("active", panel.id === targetPanel);
      });
    });
  });
}

Toggling Multiple Classes

The toggle method works on one class at a time. To toggle multiple classes, call it multiple times or use a helper:

javascriptjavascript
// Toggle each class individually
element.classList.toggle("expanded");
element.classList.toggle("highlighted");
element.classList.toggle("animate");
 
// Helper function for toggling multiple classes
function toggleClasses(element, ...classNames) {
  classNames.forEach(name => element.classList.toggle(name));
}
 
toggleClasses(card, "expanded", "highlighted", "animate");
 
// Helper with force parameter
function setClasses(element, force, ...classNames) {
  classNames.forEach(name => element.classList.toggle(name, force));
}
 
setClasses(card, true, "visible", "fade-in"); // Add all
setClasses(card, false, "visible", "fade-in"); // Remove all

Common Mistakes to Avoid

Mistake 1: Ignoring the Return Value

javascriptjavascript
// VERBOSE: Separate toggle and check
element.classList.toggle("active");
if (element.classList.contains("active")) {
  updateIcon("active");
} else {
  updateIcon("inactive");
}
 
// CLEAN: Use the return value directly
const isActive = element.classList.toggle("active");
updateIcon(isActive ? "active" : "inactive");

Mistake 2: Using Toggle When You Need Set

javascriptjavascript
// BUG: If called multiple times, state becomes unpredictable
function showNotification() {
  notification.classList.toggle("visible"); // Might HIDE it if already visible!
}
 
// CORRECT: Use force=true when you always want to add
function showNotification() {
  notification.classList.toggle("visible", true);
  // Or simply: notification.classList.add("visible");
}
javascriptjavascript
// INCOMPLETE: Only toggles the panel
button.addEventListener("click", () => {
  panel.classList.toggle("expanded");
});
 
// COMPLETE: Toggle all related elements and attributes
button.addEventListener("click", () => {
  const isExpanded = panel.classList.toggle("expanded");
  button.classList.toggle("active", isExpanded);
  button.setAttribute("aria-expanded", isExpanded);
  icon.classList.toggle("rotated", isExpanded);
});

Real-World Example: Notification Center

javascriptjavascript
function createNotificationCenter() {
  const bellButton = document.getElementById("bell-icon");
  const panel = document.getElementById("notification-panel");
  const badge = document.getElementById("notification-badge");
  let unreadCount = 0;
 
  // Toggle notification panel
  bellButton.addEventListener("click", (event) => {
    event.stopPropagation();
    const isOpen = panel.classList.toggle("open");
    bellButton.classList.toggle("active", isOpen);
 
    if (isOpen) {
      markAllAsRead();
    }
  });
 
  // Close panel when clicking outside
  document.addEventListener("click", (event) => {
    if (!panel.contains(event.target)) {
      panel.classList.remove("open");
      bellButton.classList.remove("active");
    }
  });
 
  function addNotification(title, message) {
    const item = document.createElement("div");
    item.className = "notification-item unread";
 
    const titleEl = document.createElement("strong");
    titleEl.textContent = title;
 
    const messageEl = document.createElement("p");
    messageEl.textContent = message;
 
    const timeEl = document.createElement("time");
    timeEl.textContent = "Just now";
 
    item.appendChild(titleEl);
    item.appendChild(messageEl);
    item.appendChild(timeEl);
 
    // Click to toggle read/unread
    item.addEventListener("click", () => {
      const wasUnread = item.classList.toggle("unread");
      unreadCount += wasUnread ? 1 : -1;
      updateBadge();
    });
 
    panel.querySelector(".notification-list").prepend(item);
    unreadCount++;
    updateBadge();
  }
 
  function markAllAsRead() {
    panel.querySelectorAll(".notification-item.unread").forEach(item => {
      item.classList.remove("unread");
    });
    unreadCount = 0;
    updateBadge();
  }
 
  function updateBadge() {
    badge.textContent = unreadCount;
    badge.classList.toggle("visible", unreadCount > 0);
  }
 
  return { addNotification, markAllAsRead };
}
 
const notifications = createNotificationCenter();
Rune AI

Rune AI

Key Insights

  • Return value: classList.toggle() returns true when the class is added and false when removed, eliminating the need for separate contains() checks
  • Force parameter: toggle("class", boolean) acts as a conditional add/remove, replacing if/else blocks with a single line
  • CSS-first approach: Define all visual states in CSS with transitions; use JavaScript only to toggle the class that activates them
  • Accessibility: Always update aria-expanded, aria-selected, and other ARIA attributes alongside class toggles
  • One class at a time: Unlike add() and remove(), toggle() accepts only one class name per call; loop for multiple classes
RunePowered by Rune AI

Frequently Asked Questions

Does classList.toggle() work with CSS transitions?

Yes. When `toggle()` adds or removes a class, the browser detects the style change and applies any CSS transitions defined for the affected properties. Define your transition in CSS (e.g., `transition: opacity 0.3s ease`), and the toggle triggers it automatically without any additional JavaScript.

Can I toggle multiple classes in one call?

No. Unlike `classList.add()` and `classList.remove()` which accept multiple arguments, `classList.toggle()` only accepts one class name per call. To toggle multiple classes, call `toggle()` once for each class or create a helper function that loops through an array of class names.

What happens if I pass an empty string to toggle?

Passing an empty string throws a `SyntaxError` in most browsers. Always pass a valid, non-empty class name to `toggle()`. If you are building class names dynamically, validate that the string is not empty before calling the method.

How is the force parameter different from using add/remove?

The force parameter combines conditional logic with the toggle call: `toggle("class", condition)` is equivalent to `if (condition) add("class") else remove("class")` but returns a boolean indicating the new state. This makes it more concise and you avoid writing if/else blocks. The return value also tells you what happened, which `add` and `remove` do not provide.

Should I use toggle() or add()/remove() for one-directional state changes?

Use `add()` when you always want the class present and `remove()` when you always want it gone. Use `toggle()` only when the action flips between two states (like clicking a button that opens or closes a menu). Using `toggle()` for one-directional changes (like opening a modal) can cause bugs if the function is called again unexpectedly.

Conclusion

The classList.toggle() method is the foundation of interactive CSS class management in JavaScript. Its ability to flip a class on and off in a single call, return the resulting state as a boolean, and accept a force parameter for conditional toggling makes it the ideal tool for building UI interactions. Pair it with CSS transitions for smooth animations, keep visual styles in your stylesheet rather than in JavaScript, and always update ARIA attributes alongside class changes for accessibility. For one-directional state changes, prefer classList.add() and classList.remove(); for on/off switches, toggle() is the cleanest approach.