Tackling Tailwind CSS Class Precedence Issues with 'cva' and 'cx'

Struggling with class precedence in Tailwind CSS? Learn how to overcome styling conflicts and improve code maintainability by using cva and cx. This guide explores structured styling with cva variants and dynamic classes with cx, making it easier to manage complex, conditional styles effectively.

Ajirthan Balasingham
Blog post cover image
In modern frontend development, Tailwind CSS has revolutionized styling with its utility-first approach. However, as your project scales, managing utility classes and maintaining order of precedence can become challenging. One common issue is Tailwind’s class precedence — where the order of classes determines which styles are applied, often leading to unexpected styling bugs. Thankfully, we have tools like cva (Class Variance Authority) and cx (Classnames utility) to help us create modular, predictable styles that overcome these precedence issues.
This article will guide you through understanding Tailwind CSS class precedence, then demonstrate how 'cva' and 'cx' can help you organize and manage styles effectively.
Understanding Tailwind CSS Class Precedence
In Tailwind CSS, utility classes are applied based on the order in which they appear. For example, if you write:
html
<div class="bg-blue-500 bg-red-500"></div>
The element will have a red background because bg-red-500 appears later in the list. While this behavior is logical for most scenarios, it becomes challenging as your code grows, especially when multiple components rely on dynamic or conditional classes.
Consider a more complex example:
html
<div className={`p-4 ${isActive ? 'bg-green-500' : 'bg-gray-200'} hover:bg-blue-700`}>
  Click me!
</div>
Here, depending on the isActive state, the background color will be green or gray, but the hover color will always be bg-blue-700. This logic can become cumbersome as conditions stack up. Small changes can lead to unexpected styles because of class precedence.
Introduction to cva and cx
To tackle this problem, we can utilize the following two libraries:
1. Class Variance Authority (cva): cva is a utility that helps organize and manage class names by defining variants. It enables you to build reusable class groupings based on different states, creating a more scalable, modular approach to styling.
2. Classnames (cx): cx is a function that conditionally joins class names together, adding only the classes you specify based on component state. This is ideal for handling conditional styling dynamically.
Together, cva and cx provide a flexible approach to managing Tailwind styles with a predictable class precedence, improving readability and maintainability.
Setting Up cva and cx
First, let’s install these utilities:
bash
npm install class-variance-authority classnames
Once installed, you can start using cva and cx to manage styles in a more structured manner.
Using cva for Variants and Conditional Styles
cva allows you to create reusable styling configurations, ideal for complex components where different states require different classes.
Step 1: Defining Variants with cva
Let’s say you have a button that changes styles based on its state (primary, secondary, disabled). You can define these variants using cva:
jsx
import { cva } from 'class-variance-authority';

const buttonStyles = cva('px-4 py-2 rounded', {
  variants: {
    intent: {
      primary: 'bg-blue-500 text-white hover:bg-blue-600',
      secondary: 'bg-gray-300 text-black hover:bg-gray-400',
    },
    size: {
      small: 'text-sm',
      large: 'text-lg',
    },
    disabled: {
      true: 'bg-gray-200 text-gray-500 cursor-not-allowed',
      false: '',
    },
  },
  defaultVariants: {
    intent: 'primary',
    size: 'small',
    disabled: false,
  },
});
This configuration defines a set of variants based on intent, size, and disabled states. With cva, you can specify a default variant while also leaving room for customizations.
Step 2: Applying cva to a Component
You can use this buttonStyles object within a component by passing it options:
jsx
import React from 'react';

function Button({ intent, size, disabled, children }) {
  return (
    <button className={buttonStyles({ intent, size, disabled })} disabled={disabled}>
      {children}
    </button>
  );
}
This structure ensures that each button’s style is controlled by a centralized configuration, making updates simple and intuitive. You can now apply conditional classes without worrying about precedence issues.
Using cx for Dynamic Class Handling
While cva is fantastic for predefined variants, cx shines when you need more granular control over class combinations.
jsx
import { cx } from 'classnames';

function Alert({ type, isDismissed }) {
  return (
    <div className={cx('p-4 rounded', {
      'bg-green-500 text-white': type === 'success',
      'bg-red-500 text-white': type === 'error',
      'hidden': isDismissed,
    })}>
      {type === 'success' ? 'Operation successful' : 'An error occurred'}
    </div>
  );
}
Here, cx lets you conditionally apply classes based on the type and isDismissed state. This approach allows for more dynamic styling, particularly when managing conditional classes that vary across a component’s lifecycle.
Combining cva and cx
In complex components, you might find yourself needing both cva for structured, reusable variants and cx for on-the-fly class adjustments.
jsx
import { cva } from 'class-variance-authority';
import { cx } from 'classnames';

const cardStyles = cva('rounded-lg shadow-md p-4', {
  variants: {
    intent: {
      info: 'bg-blue-100 border-blue-500',
      warning: 'bg-yellow-100 border-yellow-500',
      danger: 'bg-red-100 border-red-500',
    },
    hasBorder: {
      true: 'border',
      false: '',
    },
  },
  defaultVariants: {
    intent: 'info',
    hasBorder: true,
  },
});

function Card({ intent, hasBorder, isDismissed }) {
  return (
    <div className={cx(cardStyles({ intent, hasBorder }), { hidden: isDismissed })}>
      <p>This is a {intent} card</p>
    </div>
  );
}
In this example, cardStyles is managed by cva for handling variants, while cx dynamically hides the card when isDismissed is true. This combination offers structured styles and flexibility, giving you the best of both worlds.
Conclusion
Managing Tailwind CSS class precedence can be a hassle as your project grows. By adopting cva for variant-based styling and cx for dynamic class management, you can maintain control over your styling while avoiding unexpected precedence issues. This structured approach not only makes your codebase more maintainable but also enhances readability and scalability, allowing you to focus on building and styling your components with confidence.