Create better reusable components for your Tailwind design system
In this tutorial, we're going to build a reusable component using Tailwind CSS for a custom design system. To begin with, we'll need a basic Vite.js setup with React, TypeScript, and TailwindCSS or you can also use Nextjs setup.
We need to install a few libraries like tailwind-merge, clsx, and class-variance-authority by running the command given below in the terminal in your project directory.
yarn add tailwind-merge clsx class-variance-authority
// or //
pnpm i tailwind-merge clsx class-variance-authority
Let's understand those libraries first
tailwind-merge: It's a Tailwindcss utility class for merging multiple tailwind classes into a single string without style conflicts. For example:
import {twMerge} from 'tailwind-merge' twMerge('px-2 py-1 bg-red hover:bg-dark-red', 'p-3 bg-[#B91C1C]') // → 'hover:bg-dark-red p-3 bg-[#B91C1C]'
clsx: It's a utility for constructing className conditionally. So by using this, you can write conditional classes in a better and more readable way. For example:
import {clsx} from 'clsx' clsx('foo', true && 'bar') // -> 'foo bar' clsx('foo', false && 'bar') // -> 'foo'
class-variance-authority: this will help you with writing type-safe UI components and gives you an easy-to-use interface to define variants. It's more like stitches.js but for people who want to use custom CSS or want to use frameworks like Tailwindcss.
Configuring our utilities
As previously discussed, clsx
and tailwind-merge
help to write readable and maintainable tailwindcss classes. We create a small utility function called cn
that we will use later for our components.
// src/utilities/index.ts
import { twMerge } from 'tailwind-merge';
import { ClassValue, clsx } from 'clsx';
export function cn(...inputs: ClassValue[]){
return twMerge(clsx(inputs));
}
In this file, we have imported two functions twMerge and clsx. Where this function takes input as an array of ClassValue
type. This input is passed to clsx
function which outputs multiple classes and then twMerge
combined those classes into a single string.
Once the utility function has been created, now we proceed to the reusable component. So now let's move to the components folder.
Component
// src/components/Button.tsx
import { cva, VariantProps } from 'class-variance-authority';
import { ButtonHTMLAttributes, FC } from 'react';
import { cn } from '../utils'
We have imported cva
and VariantProps
from class-variance-authority, with cva
it's easy to create style variants for components and VariantProps
create a type of these variants.
const buttonVariants = cva(
'active:scale-95 hover:scale-105 rounded-md flex items-center justify-center text-sm font-medium transition-all duration-100',
{
variants: {
intent: {
default: 'bg-blue-500 text-white hover:bg-blue-300',
outline: 'border border-blue-500 text-white hover:text-slate-300'
},
size: {
default: 'h-10 py-2 px-4',
sm: 'h-9 px-2 rounded-md'
}
},
defaultVariants: {
intent: "default",
size: "default"
}
);
At first, we declared a constant variable called buttonVariants
whose value will be the output of cva
function.
cva
accepts two inputs first one is the base value and the other is the config object, in our case base value is a basic style class for the button component. And in the config object cva
provides variants
object in which we define our variants.
The first variant we have declared is called intent
which contains two options default
and outline
and the second variant we have declared is called size
which also contains two options default
and sm
and all those options contain some sort of style for the button component.
config object also gives defaultVariants
object where you can choose the default style for your variants.
interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button: FC<ButtonProps> = ({ className, size, intent, ...props }) => {
return (
<button
className={cn(buttonVariants({ intent, size, className }))}
{...props}
/>
);
};
In the above code, we have declared a type for the component where we extend the built-in typescript type ButtonHTMLAttributes<HTMLButtonElement>
to include all the HTML button attributes. VarinatProps
is used to get all the variants type from buttonVariants
.
After that, we defined a react functional component called Button
which accepts the type of ButtonProps
. Then we deconstruct these props where className
, size
and intent
from VariantProps and all the remaining props from html button attributes.
As we return the button tag in the component and that button contains className
property and in that property we call the cn
function from the utilities. This cn
function takes a parameter where we have passed buttonVariants
with the required values inside it.
At the last, we have exported both the Button
component and buttonVariants
.
export { Button, buttonVariants };
The reason we have also exported buttonVariants
is so that we can use those styles and variants anywhere we want to for example: using those styles in an anchor tag <a />
.
Now if we use Button
component anywhere in the app we can use those type-safe variants and get those options in autocomplete.
As you can see we can use buttonVariants
from Button.tsx
file to provide those variants and styles to an anchor tag.
<a href="https://google.com"
className={buttonVariants({ size: "sm", intent: "outline" })}
>
Link
</a>
Conclusion
By creating reusable components, development teams can increase efficiency and maintain consistency in their UI development. This allows for faster development and makes you focus on the right things.
My Twitter: Miral Suthar
Github repo: Variant-component