PaoloJulian.dev - Article

go to article list

The Right Way to Make Component Variants using Tailwind

Paolo Vincent Julian
The Right Way to Make Component Variants using Tailwind banner

Photo by David Pisnoy on Unsplash

LAST UPDATED 

TLDR; This is likely the optimal approach for creating variants in react using tailwind

ts
// @/lib/cn.ts
import clsx from "clsx";
import { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
ts
// @/components/Button/Button.variants.ts
import { cva } from 'class-variance-authority';

const buttonSizes = {
  sm: "w-8 h-6",
  md: "w-12 h-8"
  lg: "w-16 h-12",
};
const buttonColors = {
  primary: "bg-blue-400",
  danger: "bg-red-400",
  warning: "bg-orange-400",
}

export const buttonVariants = cva(
	'button',
	{
	  variants: {
	    size: buttonSizes,
	    color: buttonColors
	  },
	  defaultVariants: {
	    size: 'md',
	    color: 'primary',
	  }
	}
)
tsx
// @/components/Button/Button.tsx
import cn from '@/lib/cn';
import { VariantProps } from 'class-variance-authority';
import { buttonVariants } from './Button.variants.ts'

interface Props extends VariantProps<typeof buttonVariants> {
  children: React.ReactNode;
  className?: string;
}

const Button: React.FC<Props> = ({ children, size, color, className }){
  return <button className={cn(buttonVariants({ size, color, className })}>{children}</button>
}

The Problem with Tailwind on React

1. Dynamic Classes

tsx
<button className={`bg-${color}-400`}></button>
// This would not register since the tailwind only detects classes on runtime, so it would not include bg-{dynamic}-400
  • Tailwind CSS, as a utility-first framework, doesn't natively support dynamic classes like those generated or modified at runtime. Tailwind is primarily designed to provide a consistent set of predefined utility classes for styling without the need for custom CSS.

2. Cannot override classes from parent

tsx
// App.tsx
<CustomButton className={’bg-red-400}>Test</CustomButton>

// CustomButton.tsx
<button className={classNames(’bg-blue-400, className}>{children}</button>
  • In Tailwind CSS within a React environment, there's a challenge when attempting to override certain classes from a parent component. While it may work in some cases, there's a potential issue. If a class like bg-blue-400 is rendered later in the compiled tailwind.css file, it could override the previously defined bg-red-400 class. This behavior might lead to unexpected results, as the order of class rendering can impact styling outcomes.

There are many more, but not worth discussing.

Now, Let's discuss the steps to create variants

1. Installation

bash
npm install clsx tailwind-merge class-authority-variance

2. Create the cn function

tailwind-merge makes sure that there are no conflicting classnames.

ts
// @/lib/cn.ts
import clsx from "clsx";
import { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

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

3. Create the Button.variants.ts

This is optional, but I prefer using a modular folder structure, and that's why I like to keep things separated

ts
// @/components/Button/Button.variants.ts
import { cva } from 'class-variance-authority';

const buttonSizes = {
  sm: "w-8 h-6",
  md: "w-12 h-8"
  lg: "w-16 h-12",
};
const buttonColors = {
  primary: "bg-blue-400",
  danger: "bg-red-400",
  warning: "bg-orange-400",
}

export const buttonVariants = cva(
	'button',
	{
	  variants: {
	    size: buttonSizes,
	    color: buttonColors
	  },
	  defaultVariants: {
	    size: 'md',
	    color: 'primary',
	  }
	}
)

4. Create the Button.tsx component

ts
// @/components/Button/Button.tsx
import { cn } from '@/app/_utils/tailwind-merge';
import { VariantProps } from 'class-variance-authority';
import { buttonVariants } from './Button.variants.ts'

interface Props extends VariantProps<typeof buttonVariants> {
  children: React.ReactNode;
  className?: string;
}

const Button: React.FC<Props> = ({ children, size, color, className }){
  return <button className={cn(buttonVariants({ size, color, className })}>{children}</button>
}

5. Use the component

tsx
// @/App.tsx
import Button from './components/Button'

export default App() {
  return <Button size="sm" color="warning">Warning!</Button>;
}

Conclusion

I've tried my fair share of tricks in creating variants. But guess what? This one takes the cake — and I'm not saying it's better, I'm saying it's a game-changer. Seriously, it's like finding the perfect playlist for your day. So much simpler, so much better. Try it, you won't be disappointed! 🚀

TAGS:

#best-practice

#react

#component variance

#tailwind

go to article list