Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Learning

How to efficiently manage CSS classes in React

Managing CSS classes in React can be tricky. Especially once you start conditionally updating styles based on state. In this article, Andrew outlines a few approaches and compares their merits.

Andrew Ezeani

·

13 Feb 2024

When building applications in React, we often want components to dynamically change their appearance based on user interactions. These changes are easily achieved by using conditional CSS classes, which are applied or removed based on specific conditions.

In React, these classes are commonly applied based on a component's prop values or state. The ternary operator is frequently used to manage the application of these classes. The code snippet below is an example of this common approach:

import styles from "./button.module.css"

function Button({variant}){
  return (
    <button className={`${styles.base} ${variant ? styles[variant] : ""}`}> 
       {children}    
    </button>
  )
}

export default Button;

This approach is typical for building small components. However, as the component becomes more flexible and more props are introduced to handle this flexibility, keeping track of the available CSS classes and the conditions under which they are applied becomes difficult. The button component could end up looking like this:

import styles from "./button.module.css"

function Button({variant, type, size}){  
  return (    
    <button className={`${styles.base} ${variant ? styles[variant] : ""} ${type ? styles[type] : ""} ${size ? styles[size] : ""}`}>      
      {children}    
    </button>  
  )
}

export default Button

The length of the conditional CSS classes' application logic on the element makes it difficult to understand how the CSS classes are being applied to the element. This difficulty could make debugging your code challenging. Efficiently applying CSS classes becomes essential not just for your future self but for other developers who might work on the project.

This article will explore the efficient techniques for managing conditional CSS classes in a React application.

Prerequisites

To get the most out of this article, you will need to:

  • Have basic knowledge of React

  • Be familiar with CSS modules

  • Be familiar with ES6 syntax

  • Have Node.js installed

Project setup

We will be building a button component with the following props:

  • variant: solid, outlined, and text

  • type: primary, success, and danger

  • size: sm (small), md (medium), and lg (large)

To follow along, you will need to create a new React application. You can do that by executing this command in your terminal:

npm create vite@latest <project_name> -- ---template react

Change to your project directory once it has been created, and execute the below command to install the necessary dependencies for your project:

npm install

With the necessary dependencies installed, let's make a few changes to our new React application. First, delete the App.css file. We don't need it for this project because we will use CSS modules to style the button component.

Next, create a new components directory inside the src directory. Then, create two new files in the components directory: Button.jsx and button.module.css.

Copy and paste the following CSS styles into the button.module.css file:

/* button base style */
.base {
  color: #fff;
  font-weight: bold;
  font-size: 0.75rem;
  padding: 4px;
  cursor: pointer;
  border-radius: 6px;
}

/*Button variant styles */
.solid,
.text {
  border: none;
}

.outline,
.text {
  background-color: transparent;
  color: rgb(133, 133, 133);
}

.outline {
  border: 2px solid rgb(133, 133, 133);
}

/* button types style */
.primary {
  background-color: #3b82f6;
}

.success {
  background-color: #22c55e;
}

.danger {
  background-color: #ef4444;
}

/* Compound button styles */
.solid-primary {
  background-color: #3b82f6;
}

.text-primary,
.outline-primary {
  background-color: transparent;
  color: #3b82f6;
}

.outline-primary {
  border: 2px solid #3b82f6;
}

.solid-success {
  background-color: #22c55e;
}

.text-success,
.outline-success {
  background-color: transparent;
  color: #22c55e;
}

.outline-success {
  border: 2px solid #22c55e;
}

.solid-danger {
  background-color: #ef4444;
}

.text-danger,
.outline-danger {
  background-color: transparent;
  color: #ef4444;
}

.outline-danger {
  border: 2px solid #ef4444;
}

/* button size styles */
.sm {
  font-size: 0.875rem;
  padding: 6px;
}

.md {
  font-size: 1rem;
  padding: 8px;
}

.lg {
  font-size: 1.125rem;
  padding: 10px;
}

Next, replace the CSS styles in the index.css file with the following:

* {  
  padding: 0;  
  margin: 0;  
  box-sizing: border-box;
}

body {    
  min-height: 100vh;  
  display: grid;  
  place-items: center;
}

Next, copy and paste the following code into the Button.jsx file:

import styles from "./button.module.css"

function Button(){  
  return <button className={styles.base}>Button</button>  
}

export default Button;

Next, replace the contents of the App.jsx file with the following:

import Button from "./components/Button";

function App() {
  return <Button />;
}

export default App;

Finally, save all changes and execute the below command to start the development server:

npm run dev

You should see the below button on your screen:

A light gray, slightly rounded rectangular button with the text 'button' in white, displaying only the base styles.

With the project all set up, let's look at the different methods for efficiently managing the application of conditional CSS classes in React.

Method 1: Manual method

The manual method involves creating an array of CSS classes and then using the Array.join() method to concatenate the classes into a string that will be applied to the component. Implementing this method in our button component:

import styles from "./button.module.css";

// Set the default values for some props
function Button({ variant = "solid", size = "md", type, children }) {
  // Create an array of class names based on props
  const classNames = [
    styles.base, // Base class
    styles[size], // Size-specific class
    styles[variant], // Variant-specific class
    styles[`${variant}-${type}`], // Variant and type-specific class
  ];

  // Concatenate the classes into a string
  const btnStyles = classNames.join(" ");

  return <button className={btnStyles}>{children}</button>;
}

In the above code snippet, we created a classNames array that contains all CSS classes used for styling the button. Then, we used the join() method to concatenate the array elements into a string.

We used the join() method instead of the toString() method because the string returned by the toString() method uses a comma as the delimiter to concatenate the CSS classes in the array. When applied to the element, this fails to produce the expected styles. For example, using the toString() method:

import styles from "./button.module.css";  

// Set the default values for some props
function Button({ variant = "solid", size = "md", type, children }) {  
  
  // Create an array of class names based on props  
  const classNames = [ 
    styles.base,  // Base class    
    styles[size],  // Size-specific class    
    styles[variant],  // Variant-specific class    
    styles[`${variant}-${type}`] // Variant and type-specific class
  ];
  
  // Using the toString method to Concatenate the classes into a string  
  const btnStyles = classNames.toString();
  
  return <button className={btnStyles}>{children}</button>;
}

We get this button once we save the changes:

A HTML button element with default styling and displaying the text button.

When we inspect the element in the browser DevTools:

A devtools snapshot showing a button element with a single class that includes commas: "base_qizaw_2,sm_qizaw_84,solid_qizaw_13" due to JavaScript's toString method.

The classes are separated by a comma and applied on the button as a single class instead of separate classes. Using the join() method, we can pass a delimiter of our choice as an argument, in this case, a space, when we call the join() method.

When we revert our changes and save the file, we get this nice-looking button in the browser:

The primary button, a blue rectangular button with rounded corners and the text 'button' in white.

Pros of the manual method

  • Simple implementation: The manual method uses basic JavaScript, which makes its implementation easy as it requires no learning curve.

  • Improved code clarity: When contrasted with the inline method that uses ternary operators, it's easy to understand the code flow, making it a bit easier to debug.

  • No external dependencies: It doesn't rely on external libraries, reducing project dependencies.

Cons of the manual method

  • Code verbosity: The manual method may become less maintainable when dealing with more complex styling scenarios or larger projects. As the number of conditions and styles increases, the code could become convoluted and harder to manage.

  • No explicit conditions: It’s difficult to understand the conditions under which different CSS classes are applied to the element, which could make debugging the code difficult.

Method 2: Using the clsx library

clsx is a lightweight utility library used to manage the application of CSS classes. It's a simple function that accepts an object, array, or string as arguments and returns a string interpolation of the valid classes based on the provided conditions.

Execute the below command in your terminal to install the clsx library:

npm install clsx

With the clsx library installed, let’s refactor our Button component:

import clsx from "clsx";
import styles from "./button.module.css";

// Setting some default prop values
function Button({ variant = "solid", size = "md", type, children }) {
  const btnStyles = clsx({
    // Define class names as object properties
    // The base class for the button is always included
    [styles.base]: true,
    // Include the variant-specific class if the 'variant' prop is provided 
    [styles[variant]]: variant,
    // Include the size-specific class if the 'size' prop is provided
    [styles[size]]: size,
    // Include a compound class if the 'type' prop is provided
    [styles[`${variant}-${type}`]]: type,
  });

  return <button className={btnStyles}>{children}</button>;
}

export default Button;

In the above code snippet, we called the clsx function and stored its return value in the btnStyles variable. We provided an object as an argument to the function, where each key represents a CSS class that is conditionally applied based on its associated values. The first class, set to a value of true, ensures that the class is always applied each time the button component is rendered. Subsequent keys are mapped to different props and are only applied when a value is passed to the respective props when the component is rendered.

You get the same nice-looking button after saving the file:

The primary button, a blue rectangular button with rounded corners and the text 'button' in white.

This approach could be further optimized, by checking if the prop has a valid value before applying the corresponding CSS class, rather than applying the CSS class associated with any of the props when the prop has a true value. This helps avoid situations where an undefined class could be applied due to an invalid prop value passed to either of the component props. To optimize this approach, we can proceed as follows:

import clsx from "clsx";
import styles from "./button.module.css";

function Button({ variant = "solid", size = "md", type, children }) {
  //Added an array of all the valid prop values
  const validVariants = ["solid", "outlined", "text"];
  const validTypes = ["primary", "success", "danger"];
  const validSizes = ["sm", "md", "lg"];

  const btnStyles = clsx({
    [styles.base]: true,
    // The classes are only applied when the prop has a true value
    // and the value is a valid option for the given prop
    [styles[variant]]: variant && validVariants.includes(variant),
    [styles[size]]: type && validSizes.includes(size),
    [styles[`${variant}-${type}`]]: type && validTypes.includes(type),
  });

  return <button className={btnStyles}>{children}</button>;
}

export default Button;

In the above code snippet, we created an array of valid values for each prop. Then, we used the && operator to ensure that the CSS class associated with a prop is only included if it has a true value and it is a valid option for the specific prop. This helps to prevent undefined CSS classes from being applied.

Pros of the clsx library method

  • Concise code: clsx eliminates the need for verbose inline conditional statements, resulting in more compact and easily understandable code.

  • Improved code clarity: clsx enhances code clarity by explicitly defining the conditions for which each class is applied to the element, making it easier to understand and maintain compared to manual methods.

  • Easier to maintain: Explicitly setting out the conditions for applying different CSS classes makes the code easier to understand and, therefore, more maintainable.

Cons of the clsx library method

  • Additional dependency: Although clsx is a small package (255B), it's still an extra dependency for your project, which should be considered.

Learning Curve: While not steep, additional time is still required to use the clsx library effectively.

Method 3: Using the class-variance-authority library

class-variance-authority (cva) is another utility library used for managing the conditional application of CSS classes in a component. The key difference between cva and clsx is the need to explicitly specify which styles are applied to the component in cva, based on the presence and combinations of the different props values provided when the component is rendered.

Execute the below command in your terminal to install the cva library:

npm install class-variance-authority 

Refactoring the Button component, we now have:

import { cva } from "class-variance-authority";
import styles from "./button.module.css";

function Button({ variant, type, size, children }) {  
  const btnStyles = cva([styles.base], {    
    variants: {      
      variant: {        
        solid: styles.solid,        
        outline: styles.outline,        
        text: styles.text,      
      },      
      type: {        
        primary: styles.primary,        
        success: styles.success,        
        danger: styles.danger,      
      },      
      size: {        
        sm: styles.sm,        
        md: styles.md,        
        lg: styles.lg,      
      },    
    },    
    compoundVariants: [      
      {        
        variant: "solid",        
        type: "danger",        
        className: styles["solid-danger"],      
      },      
      {        
        variant: "solid",        
        type: "success",        
        className: styles["solid-success"],      
      },      
      {        
        variant: "outline",        
        type: "primary",        
        className: styles["outline-primary"],      
      },      
      {        
        variant: "outline",        
        type: "danger",
        className: styles["outline-danger"],
      },      
      {        
        variant: "outline",        
        type: "success",        
        className: styles["outline-success"],      
      },      
      {        
        variant: "text",        
        type: "primary",        
        className: styles["text-primary"],      
      },      
      {        
        variant: "text",
        type: "danger",        
        className: styles["text-danger"],      
      },      
      {        
        variant: "text",
        type: "success", 
        className: styles["text-success"],  
      },    
    ],  
    defaultVariants: {    
      variant: "solid",    
      size: "md",   
    },  
  });

  return  <button className={btnStyles({ variant, type, size })}>{children}</button>  
}

export default Button;

In the above code snippet, we called the cva function with two arguments and stored its return value in the btnStyles variable, which is then invoked to return the appropriate classes. Let's break down each argument passed to the function:

  • The first argument is the CSS class, which will be applied whenever the Button component is rendered. This could either be a string or an array of class names.

  • The second argument is an object with three properties: variants, compoundVariants, and defaultVariant.

  • The variants key is mapped to an object that contains the various props as keys. Each prop is further defined with its possible values and the corresponding CSS class that should be applied when the prop matches one of these values.

  • The compoundVariants property is an array of objects, each defining a combination of valid prop values and the corresponding CSS class to apply when the prop values match any defined combinations in the compoundVariant array.

  • The defaultVariant property holds the value of the default CSS classes to be applied when the Button component is rendered with either a missing prop value or without props.

We get the same button when we save the file:

The primary button, a blue rectangular button with rounded corners and the text 'button' in white.

Pros of the cva library

  • Code clarity: explicitly setting out the classes enhances code clarity, making it easier to understand your application and debug.

  • Greater control: By explicitly defining classes for each prop value and combination, this method gives you greater control over your component's appearance.

  • Eliminates unexpected side effects: The cva library syntax eliminates unexpected side effects by ensuring that classes are set based on the prop values of the component. This prevents issues like applying undefined classes, that we experienced while using the clsx library.

Cons of the cva library

  • Learning curve: Time is required to learn how to use the cva library effectively, but the curve is not steep.

  • Lengthy boilerplate code: This method requires explicit definitions for compound variants, increasing the code needed to get started.

  • Additional dependency: The cva is an extra dependency for your project, which should be taken into consideration.

Conclusion

Efficiently managing the application of conditional CSS classes is important for building scalable and maintainable components in a React application. In this article, we explored three efficient methods for managing the application of conditional CSS classes in a React application. The advantages and disadvantages outlined will hopefully help you decide on the right approach for your next project.

Choosing the right method depends on the project's scale, complexity, and personal preference. The manual method is a good choice for small personal projects due to its simplicity and lack of a learning curve, and it adds no extra dependency to your project. clsx is a better choice if an extra dependency and the time required to learn the library are not an issue because it provides an easier-to-understand syntax, which makes debugging your application easier than the manual method. The class-variance-authority method is a better alternative if you need a deterministic way of knowing what type of element will be rendered for any given combination of props in your project.

Additionally, using CSS modules, style component libraries like Material UI (MUI), or a CSS framework like Tailwind CSS for styling your components enhances the overall maintainability of your React project because these styling options keep your styles isolated, which helps prevent style conflicts.

Ultimately, whatever method you choose should align with the specific requirements of your project and your development preferences.

Further reading

Practice building projects like a pro

  • Portfolio-ready projects
  • Professional design files
  • Curate your profile
  • Unlimited solution refinement
Unlock Pro

Get all community news and our latest articles in your inbox

Join over 50,000 developers receiving updates via our newsletter. Stay up-to-date with new challenges, articles, community news, featured solutions, and handy links from across the web. We promise no spam, and you can unsubscribe at any time.