August 13, 2024 · 4 min read · 102 views
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.
Let’s begin by setting up a new React project using Vite.
npm init vite@latest tw-components --template react-ts
After the project has been created, navigate to the project directory and install the necessary packages for Tailwind CSS:
npm install -D tailwindcss postcss autoprefixer
Next, 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 -p
Now, 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;
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@beta
With 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!
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.
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-slot
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, cva
for styling, and our utility function cn
.
Define TypeScript Interface: We define a TypeScript interface ButtonProps
that extends standard button attributes and adds custom variant props for styling. It includes an optional asChild
prop to determine if the button should act as a child component.
Button Styles: We define buttonStyles
using cva
to apply conditional and responsive styling based on the button's variant and size. This includes default styles for both variant
and size
.
Creating the Button Component: We create the Button
component using React.forwardRef
, allowing us to pass a ref through to the DOM button element. It also checks the asChild
prop to decide whether to render a button
element or use the Slot
component for custom behavior.
Apply Computed Styles: We apply the computed styles from buttonStyles
using the cn
utility function, which merges CSS classes.
That was straightforward, wasn't it?
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.
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.
If you need help with a React project let's get in touch.
Lucky Media is proud to be recognized as a Top Next.js Development Agency
Technologies:
Related Posts
Stay up to date
Be updated with all news, products and tips we share!