Creating a Reusable Button Component with React, TypeScript, and Tailwind CSS

August 13, 2024 · 4 min read

It’s been over three years since we published one of our most popular blog posts on creating a reusable button component with React and Tailwind CSS. Given the many advancements in the web development landscape, we believe it’s time for an update.
In this guide, we will create a reusable Button component using React, TypeScript, and Tailwind CSS. This approach not only enhances type safety but also simplifies project maintenance and scalability. We will use Tailwind Merge and CVA to manage dynamic class names more effectively.
Setting Up the Project
Let’s begin by setting up a new React project using Vite.
npm init vite@latest tw-components --template react-tsAfter the project has been created, navigate to the project directory and install the necessary packages for Tailwind CSS:
npm install -D tailwindcss postcss autoprefixerNext, initialize Tailwind CSS by creating the configuration files. This command will create a tailwind.config.js and a postcss.config.js file in your project.
npx tailwindcss init -pNow, we need to update our tailwind.config.js file to include the paths to your template files:
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}"
],
theme: {
extend: {},
},
plugins: [],
};Finally, update the index.css file to include Tailwind’s base, components, and utilities:
/* index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;Creating our helper function
Before we dive deeper, let’s set up a helper function that will resolve any class conflicts, making it easier to overwrite styles as needed. To do this, we will install three packages: tailwind-merge, clsx, and cva.
npm i -D tailwind-merge clsx cva@betaWith these packages installed, let’s create a lib folder in the src directory and within that, a utils.ts file to store all our helper functions.
// src/lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}A special thanks to shadcn for the helper function!

Creating the Button Component
Now, let’s set up our Button component. First, create a new folder named components inside the src directory of your project. Within this folder, create a file named Button.tsx, which will contain our Button component.
Installing Radix UI React Slot
Before we start coding the Button component, we need to add a helpful package called @radix-ui/react-slot. This package provides a component called Slot from Radix Primitives, which is incredibly useful for component composition.
The Slot component allows you to seamlessly forward props and attributes from a parent component to a child element. This functionality is particularly beneficial when working with frameworks like Next.js or Inertia.js, where you may want to style a Link component using the same styles as your Button component.
To install this package, run the following command in your project directory:
npm install @radix-ui/react-slotBuilding the Button Component
Now, let’s code the Button component:
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "cva";
import { cn } from "../lib/utils";
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonStyles> {
asChild?: boolean;
}
const buttonStyles = cva({
base: [
"whitespace-nowrap rounded-md",
"inline-flex items-center justify-center",
"text-sm font-medium transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
"disabled:pointer-events-none disabled:opacity-50",
],
variants: {
variant: {
default:
"bg-blue-700 text-white hover:bg-blue-800 focus-visible:ring-blue-300",
destructive:
"bg-red-700 text-white hover:bg-red-800 focus-visible:ring-red-300",
outline:
"border border-gray-900 text-dark-900 bg-white hover:bg-gray-900 hover:text-white",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonStyles({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
export default Button;Here’s a breakdown of how our Button component works:
Import Necessary Modules: We import essential packages and modules from React, Radix UI,
cvafor styling, and our utility functioncn.Define TypeScript Interface: We define a TypeScript interface
ButtonPropsthat extends standard button attributes and adds custom variant props for styling. It includes an optionalasChildprop to determine if the button should act as a child component.Button Styles: We define
buttonStylesusingcvato apply conditional and responsive styling based on the button’s variant and size. This includes default styles for bothvariantandsize.Creating the Button Component: We create the
Buttoncomponent usingReact.forwardRef, allowing us to pass a ref through to the DOM button element. It also checks theasChildprop to decide whether to render abuttonelement or use theSlotcomponent for custom behavior.Apply Computed Styles: We apply the computed styles from
buttonStylesusing thecnutility function, which merges CSS classes.
That was straightforward, wasn’t it?
Using the Button Component
Now that our Button component is ready, we can use it in our App.tsx file. Here’s how to do that:
import Button from "./components/Button";
export default function App() {
return (
<div className="max-w-5xl mx-auto p-16 space-x-4">
<Button>Click me</Button>
<Button variant="outline">Click me</Button>
<Button variant="danger">Click me</Button>
</div>
);
}Let’s also explore how to use this Button with a Link component from another package:
import Link from "next/link";
export default function App() {
return (
<Button asChild>
<Link href="/login">Login</Link>
</Button>
);
}This setup will automatically forward all props and attributes from our Button to the Link component.
Conclusion
By following this guide, you have successfully created a reusable Button component using React, TypeScript, and Tailwind CSS. This component is versatile, allowing for a variety of styles and sizes while maintaining type safety. You can further enhance this component by adding additional variants or props as needed.
Bring Your Ideas to Life 🚀
If you need help with a React project let’s get in touch.
Lucky Media is proud to be recognized as a leading Next.js Development Agency
Technologies

Stay up-to-date
Be updated with all news, products and tips we share!

