Create High-Quality React Components

August 21, 2023

Create high-quality, type-safe, reusable React components with TypeScript and tailwindcss.

Install Dependencies

Install necessary dependencies to make everything easier. The following example is from a Nextjs 13 project with TypeScript.

clsx

A tiny utility for constructing className strings conditionally.

npm install clsx

tailwind-merge

Utility function to efficiently merge Tailwind CSS classes in JS without style conflicts.

npm install tailwind-merge

class-variance-authority

CSS-in-TS type-safe library that allows us to easily define and apply CSS class variatons.

npm install class-variance-authority

Create Variances

We need to use the class-variance-authority to create different variances of out component, using tailwind. It is actually very simple. we just need to import {cva} from 'class-variance-authority' and declare our variances.

export const buttonVariants = cva(
	// Base props that all variants will have
	"text-white text-base font-semibold text-center rounded-lg cursor-pointer py-1",
	{
		variants: {
			variant: {
					default: "bg-accent-1 border border-accent-2",
					primaryGreen: "gradient-lr border border-accent-green",
			},
			size: {
					default: "w-auto px-2",
					fullWidth: "w-full",
					small: "w-auto px-1",
					medium: "w-auto px-2",
					large: "w-auto px-4",
			},
			defaultVariants: {
					variant: "default",
					size: "default",
			},
		},
	}
);

As you can see, we have a buttonVariants object that contains all the variances of our button component. The first line of styles are the base styles that all variances will have. Then we have the variants object that contains all the different variances. In this case, we have variant and size.

  • variant is the different color schemes that our button can have.
  • size is the different sizes that our button can have. DefaultVariants is where we define the default values for our variances.

We export our buttonVariants object so that we can use it other components, and share the same variances.

Util function (tailwind-merge)

Firs we need to import clsx and twMerge from their respective packages, and then we can create a function that will merge our classes.

import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));

This function will take in any number of classes, and merge them together. This is useful when we applyu our variances or if we want to add extra classes to our component.

Types

Let's create an interface for our component, in this case, a button.

import { ButtonHTMLAttributes } from "react";
import { VariantProps } from "class-variance-authority";
export interface ButtonProps
  extends ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {}

The interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> so that we can use all the props that a button element can have. Then we extend VariantProps<typeof buttonVariants> so that we can use all the variances that we defined in our buttonVariants object.

Component

Now we can create our component. We will use the cn function that we created earlier to merge our classes.

import { forwardRef } from "react";
import { cn } from "./utils";
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
	({ className, size, variant, ...props }, ref) => {
		return (
			<button
				ref={ref}
				className={cn(buttonVariants({ className, size, variant }))}
				{...props}
			/>
		);
	}
);

As you can see, we import forwardRef from react, this is so that we can use refs in our component, because ref cannot be passed as a prop. We use forwardRef<HTMLButtonElement, ButtonProps> to forward the ref to the button element, and to use our ButtonProps interface. We destructure className, size, and variant from props so that we can use them in our cn function. By getting ...props we can use all the default props that a button element can have.

Usage

Now we can use our component in our app.

import { Button } from "./Button";
export default function App() {
	return (
		<div className="flex flex-col items-center justify-center min-h-screen py-2">
			<Button variant="primaryGreen" size="large">
				Button Text
			</Button>;
		</div>
	);
}