Back to writing

🛠️

cn(): The Most Useful Function for Styling

July 31, 2024

5 min read

Cover

# The Problem

When writing CSS class names in React's className, we often find that we want to merge the names of two identical CSS classes. For instance, consider a component like this:

Maxwidthdiv.tsx
type Props = React.HTMLAttributes<HTMLDivElement>
 
export function Maxwidthdiv({
  className,
  children,
  ...props
}: Props) {
  return (
    <div
      className={`mx-auto w-full max-w-[1440px] px-8 sm:px-10 md:px-12 ${className}`}
      {...props}
    >
      {children}
    </div>
  );
}
tsx

We want to use Maxwidthdiv flexibly, so we need to use the className prop. However, if we write className as above, there's a chance the classes we write won't merge well. For example, if the Maxwidthdiv component is called like this:

export default function App() {
  return (
    <Maxwidthdiv className="text-primary font-bold px-4">
      <p>Something</p>
    </Maxwidthdiv>
  )
}
tsx

The App component uses Maxwidthdiv with the class px-4, while inside Maxwidthdiv, there's also px-8. This can cause inconsistent styling results, sometimes resulting in px-4, and other times px-8, depending on how Tailwind processes the sorting.

# Solution: twMerge

Github tailwind-merge

Github tailwind-merge

To merge classes in TailwindCSS, we can use the twMerge function from the tailwind-merge package. First, we need to install tailwind-merge:

npm install tailwind-merge
bash

Then, we can use twMerge like this:

import { twMerge } from 'tailwind-merge'
 
type Props = React.HTMLAttributes<HTMLDivElement>
 
export function Maxwidthdiv({
  className,
  children,
  ...props
}: Props) {
  return (
    <div
      className={twMerge('mx-auto w-full max-w-[1440px] px-8 sm:px-10 md:px-12', className)}
      {...props}
    >
      {children}
    </div>
  );
}
tsx

With the updated Maxwidthdiv component, if it's called in the App component as before, there will be no ambiguity in the results. The final outcome will definitely use px-4 in the App.

# Handling Complex Combinations with clsx

Github clsx

Github clsx

If our class notation is complex and full of conditional statements, it would be better to use clsx. We can install it with:

npm install clsx
bash

Based on the clsx documentation, we can perform several complex combinations like this:

import clsx from 'clsx';
// or
import { clsx } from 'clsx';
 
// Strings (variadic)
clsx('foo', true && 'bar', 'baz');
//=> 'foo bar baz'
 
// Objects
clsx({ foo:true, bar:false, baz:isTrue() });
//=> 'foo baz'
 
// Objects (variadic)
clsx({ foo:true }, { bar:false }, null, { '--foobar':'hello' });
//=> 'foo --foobar'
 
// Arrays
clsx(['foo', 0, false, 'bar']);
//=> 'foo bar'
 
// Arrays (variadic)
clsx(['foo'], ['', 0, false, 'bar'], [['baz', [['hello'], 'there']]]);
//=> 'foo bar baz hello there'
 
// Kitchen sink (with nesting)
clsx('foo', [1 && 'bar', { baz:false, bat:null }, ['hello', ['world']]], 'cya');
//=> 'foo bar hello world cya'
ts

# cn() = twMerge + clsx

twMerge helps us merge classes, while clsx helps us combine complex statements into a class string. With both functions, we can create a new function named cn() like this:

import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
 
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
ts

With the cn() function, we can perform complex combinations like this:

// 1
cn('text-base font-medium', true && 'font-bold p-10', 'p-4');
// => 'text-base font-bold p-4'
 
// 2
const isLarge = true;
cn({ 'text-lg font-bold': isLarge, 'text-sm': !isLarge, 'font-medium text-wrap': true });
// => 'text-lg font-medium text-wrap'
 
// 3
cn({ 'bg-blue-500 text-black': true }, { 'text-white': true }, null, { 'rounded-lg': true });
// => 'bg-blue-500 text-white rounded-lg'
ts

Now we can update the Maxwidthdiv component to use the cn() function:

import { cn } from 'path-to-cn'
 
type Props = React.HTMLAttributes<HTMLDivElement>
 
export function Maxwidthdiv({
  className,
  children,
  ...props
}: Props) {
  return (
    <div
      className={cn('mx-auto w-full max-w-[1440px] px-8 sm:px-10 md:px-12', className)}
      {...props}
    >
      {children}
    </div>
  );
}
tsx

In this way, we use the Maxwidthdiv component with a class that has complex combinations like this:

export default function App() {
  const [isCollapsed, setIsCollapsed] = useState(false)
 
  return (
    <Maxwidthdiv
      className={
        cn(
          "text-primary font-bold px-4",
          isCollapsed ? 'w-40 px-1' : 'w-[200px]'
        )
      }
    >
      <p>Something</p>
      <button onClick={() => setIsCollapsed(prev => !prev)}>
        Collapsed
      <button>
    </Maxwidthdiv>
  )
}
tsx

# Conclusion

TailwindCSS is a utility-first CSS framework, so we need to write many CSS classes in React components' className. By using the cn() function, we can make the CSS class notation clear and handle complex conditional statements with the help of twMerge and clsx.

Okay... Maybe that's all for my explanation about the most useful function for styling, cn(). Have you ever used the cn() function before? Or do you have an alternative? You can write it in the comment section below 👇!

# Extra: shadcn-ui

Landing Page shadcn-ui

Landing Page shadcn-ui

I first learned about the cn() function actually from shadcn-ui. By installing shadcn-ui, we will obtain the cn() function in the file /src/lib/utils.ts. The cn() function is used in almost all shadcn-ui components aimed at enabling us to style flexibly.

After thinking about it again, is it possible that the "cn" in "shadcn" is actually taken from this cn() function... Hmm... 🤔