Hero
back button

Back to all articles

Tailwind utilities you should know

9 min read
📝 1304 words
Tailwind utilities you should know

TailwindCSS is a utility-first css framework, which uses CSS classes for rapid development.

In this tutorial we are going to look at two utilities for Tailwind CSS:

  1. Tailwind Merge npm, github
  2. clsx npm, github

Tailwind Merge

Tailwind merge is a utility function to efficiently merge Tailwind CSS classes in JS without style conflicts

If you use tailwind css with a component-based UI renderer like React or Vue, you're probably familiar with the situation that you want to change some styles of a component, but only in a one-off case.

For eg.


// React components with JSX syntax used in this example

function MyGenericInput(props) {

    // concatenate props.className to the existing classes
    const className = `border rounded px-2 py-1 ${props.className || ''}`;
    return <input {...props} className={className}> 
}

function MyOneOffInput(props) {
    return {
        <MyGenericInput 
            {...props}
            className="p-3" // <- only want to change some padding of the generic input component 
        >
    }
}

When MyOneOffInput is rendered, an input with the className border rounded px-2 py-1 p-3 gets created. But because of the way the CSS cascade works, the styles of the p-3 class are ignored. The order of the classes in the className string doesn't matter at all and the only way to apply the p-3 styles is to remove both px-2 and py-1.

This is where tailwind merge comes in.

Tailwind Merge is a 23.2kB (minified) bundle size utility function. it supports Tailwind v4.0 and works in all mordern browsers and maintained node versions, also its fully typed so you can easily integrate with Typescript for better developer experience.

Install tailwind-merge in your react project using the below command.

npm i tailwind-merge
import { twMerge } from 'tailwind-merge'

function MyGenericInput(props) {
    // Now `props.className` can override conflicting classes
    const className = twMerge('border rounded px-2 py-1', props.className)

    // new classes -> border rounded p-3
    return <input {...props} className={className}> 

}


function MyOneOffInput(props) {
    return {
        <MyGenericInput 
            {...props}
            className="p-3" // <- only want to change some padding of the generic input component 
        >
    }
}

tailwind-merge overrides conflicting classes and keeps everything else untouched. In the case of the ** MyOneOffInput** , the input is now rendered with the classes border rounded p-3.

Joining Internal classes

If you want to merge classes that are defined within a component, prefer using the twJoin function over twMerge. As the name suggests twJoin only joins the class strings together and doesn't deal with conflicting classes.


// React components with JSX syntax used in this example

import { twJoin } from 'tailwind-merge';

function MyComponent({ forceHover, disabled, isMuted }) {
    return (
     <div
        className={twJoin(
                TYPOGRAPHY_STYLES_LABEL_SMALL,
                'grid w-max gap-2',
                forceHover ? 'bg-gray-200' : ['bg-white', !disabled && 'hover:bg-gray-200'],
                isMuted && 'text-gray-600',
            )}
        >
            {/* More code… */}
        </div>
    )
}

Joining classes instead of merging forces you to write your code in a way so that no merge conflicts appear which seems like more work at first. But it has two big advantages :

  1. It's much more performant because no conflict resolution is computed. twJoin has the same performance characteristics as other class joining libraries.

  2. It's usually easier to reason about. When you can't override classes, you naturally start to put classes that are in conflict with each other closer together through conditionals like ternaries. Also when a condition within the twJoin call is truthy, you can be sure that this class will be applied without the need to check whether conflicting classes appear in a later argument. Not relying on overrides makes it easier to understand which classes are in conflict with each other and which classes are applied in which cases.

Example :

Let's say we are making a button component styled with Tailwind

<button
  type="button"
  className="relative inline-flex items-center border border-zinc-300 bg-white px-4 py-2 text-sm font-semibold text-zinc-700"
>
  Hello, world.
</button>

Pretty good so far. However, that's just the base style for the button. Let's try adding some hover styles, so that the button changes color when the mouse is hovering over it:

<button
  type="button"
  className="relative inline-flex items-center border border-zinc-300 bg-white px-4 py-2 text-sm font-semibold text-zinc-700 transition-colors hover:bg-zinc-50"
>
  Hello, world.
</button>

Not too bad. Now let's try adding some focus styles, so that the button changes color when it's focused:

<button
  type="button"
  className="relative inline-flex items-center border border-zinc-300 bg-white px-4 py-2 text-sm font-semibold text-zinc-700 transition-colors hover:bg-zinc-50 focus:z-10 focus:border-teal-500 focus:outline-none focus:ring-1 focus:ring-teal-500"
>
 Hello, world.
</button>

Getting a bit intense now. How about dark mode support?

<button
  type="button"
  className="relative inline-flex items-center border border-zinc-300 bg-white px-4 py-2 text-sm font-semibold text-zinc-700 transition-colors hover:bg-zinc-50 focus:z-10 focus:border-teal-500 focus:outline-none focus:ring-1 focus:ring-teal-500 dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800 dark:focus:border-teal-400 dark:focus:ring-teal-400"
>
  Hello, world.
</button>

Now it's getting a bit ridiculous. This is just a simple button, and it's already got a ton of classes. What if we want to add a disabled state? Or a loading state? Or a state where the button is both disabled and loading? How many classes do we need to add to the button to account for all these different states?

That's when twJoin comes in

import { twJoin } from 'tailwind-merge';

<button
  type="button"
  className={twJoin(

    // Base styles
    'relative inline-flex items-center border px-4 py-2 text-sm font-semibold transition-colors focus:z-10 focus:outline-none focus:ring-1',

    // Light-mode focus state
    'focus:border-teal-500 focus:ring-teal-500',

    // Dark-mode focus state
    'dark:focus:border-teal-400 dark:focus:ring-teal-400'

    value === item.value

      // Selected / hover states
      ? 'border-teal-500 bg-teal-500 text-white hover:bg-teal-600'

      // Unselected / hover state
      : 'border-zinc-300 bg-white text-zinc-700 hover:bg-zinc-50',

    value !== item.value &&

      // Dark-mode unselected state (selected is the same)
      'dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800',
  )}
>
  Hello, world.
</button>

clsx

clsx is a tiny (239B) utility for constructing className strings conditionally. While Tailwind merge solves the problem of conflicting classes, some developers prefer to use an object-based syntax for conditional classes. This is where the clsx library comes in handy.

Handling Conditional classes

Another common scenario is when you need to apply different classes based on a condition, such as component's state. Tailwind merge makes this easy to manage as well:

const buttonClasses = twMerge('bg-blue-500 text-white px-4 py-2 rounded', 'bg-green-500', loading && 'bg-gray-500');

By using clsx, you can now define your conditional classes in an object-based format, which some developers find more intuitive.

Install clsx in your project :

npm install --save clsx

Usage

The clsx function can take any number of arguments, each of which can be an Object, Array, Boolean or String.

Any falsey values are discarded ! Standalone Boolean values are discarded as well.

import { clsx } from 'clsx';

clsx (true, false, '', null, undefined, 0, NaN); // => '' 

// strings
clsx ('foo', true && 'bar', 'baz'); // => 'foo bar baz'

// objects
clsx ({foo: true, bar: false, baz: isTrue() }); // => 'foo baz'

// arrays
clsx (['foo', 0, false, 'bar']); // => 'foo bar'
clsx (['foo'], ['', 0, false, 'bar'], [['baz', [['hello'], 'there' ]]]); // => 'foo bar baz hello there'

From the previous example of buttonClasses.


import clsx from 'clsx';

const buttonClasses = twMerge(
    clsx({
        'bg-blue-500 cursor-now-allowed': !loading, // when false, use this classes
        'bg-gray-500 cursor-pointer': loading,  // when true, use this classes
    }),
    'text-white px-4 py-2 rounded'
);

Combining the powers of Tailwind Merge and clsx

To get the best of both worlds, you can combine Tailwind Merge and clsx using a custom utility function:

utils.js file (if you are not using typescript)


import { clsx } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs) {
  return twMerge(clsx(inputs))
}

util.ts file (if you are using typescript)

import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Great now you can import the cn utility function any where and pass your classes like this: