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.
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
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):
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.
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:
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):
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:
// 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:
// 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
| Scenario | toggle() | add() / remove() |
|---|---|---|
| Switch between on/off states | toggle("active") | Requires if/else check |
| Always set to on | toggle("active", true) | add("active") |
| Always set to off | toggle("active", false) | remove("active") |
| Set based on condition | toggle("active", condition) | if/else with add/remove |
| Need the resulting state | Returns boolean | Must call contains() after |
Practical UI Patterns
Pattern 1: Dark Mode Toggle
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";
});
}: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
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");
}
});
}
});
}.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
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
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;
});
}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
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:
// 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 allCommon Mistakes to Avoid
Mistake 1: Ignoring the Return Value
// 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
// 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");
}Mistake 3: Forgetting to Toggle Related Elements
// 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
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
Key Insights
- Return value:
classList.toggle()returnstruewhen the class is added andfalsewhen removed, eliminating the need for separatecontains()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()andremove(),toggle()accepts only one class name per call; loop for multiple classes
Frequently Asked Questions
Does classList.toggle() work with CSS transitions?
Can I toggle multiple classes in one call?
What happens if I pass an empty string to toggle?
How is the force parameter different from using add/remove?
Should I use toggle() or add()/remove() for one-directional state changes?
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.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.