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

lokman musliu
Lokman Musliu

August 13, 2024 · 4 min read · 102 views

React and TailwindCSS Blogpost Image

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-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;

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@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!

React 19 release date logo

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-slot

Building 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, 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?

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 Top Next.js Development Agency

lokman musliu
Lokman Musliu

Founder and CEO of Lucky Media

Technologies:

React
TailwindCSS
Heading Pattern

Related Posts

Stay up to date

Be updated with all news, products and tips we share!