Creating a reusable Button component with React and Tailwind

Lokman Musliu Founder and CEO of Lucky Media
Lokman Musliu

August 11, 2021 · 5 min read

Creating a reusable Button component with React and Tailwind

2025 UPDATE

This article is one of our most popular blog posts. Given the many advancements in web development, we believe it’s time for an update. That’s why we’ve refactored the code and offered new techniques for Creating a Reusable Button Component with React, TypeScript, and Tailwind CSS. Feel free to keep reading if you want to compare what we had before with what we suggest now.

Reusable components with React and TailwindCSS

For the past three years, we’ve chosen TailwindCSS, a utility-first CSS framework, for many projects. As we grow our web app development, it’s clear we need to make reusable components. This article will explain how we make a Button component with React and TailwindCSS.

Using TypeScript for UI components

While TypeScript has clear benefits for creating UI components, like interfaces and enums, and makes prop validation easier, we’re sticking to the basics in this guide. So, we’re using plain React for clearness and simplicity. But stay tuned for a possible TypeScript version of this guide, which we might put out to meet the needs of those who like the extra type safety and development tools that TypeScript offers.

Installation

The first step in our process is to kick off a new React project. For our development environment, we’ve opted for Vite, renowned for its speed and efficiency.

npm init vite@latest tw-components --template react

After the required packages have been installed we proceed to install Tailwind by following their official setup guide.

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

We create the config file and PostCSS setup with the command:

npx tailwindcss init -p

Now after we have the tailwind.config.js file we need to update it in order for the JIT plugin to work correctly. We add mode: 'jit' and update the purge key.

module.exports = {
  purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  darkMode: false,
  mode: 'jit', // add this
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

Last but not least, we update the index.css file in order to add the Tailwind styles.

@tailwind base;
@tailwind components;
@tailwind utilities;

After importing the three directives, we will cleanup our project a bit, remove the App.css file, and update the App.jsx file so we can insert our Button component inside of it.

import React from 'react'

function App() {
  return (
    <div className="container mx-auto py-32">
      {/* Buttton will be here */}
    </div>
  )
}

export default App

We create a folder called components inside the src directory and add a Button.jsx file.

We add some base props for our Button but you can extend them depending on the scope, in our case we will be adding children, type, className, variant, size, pill, disabled and the rest of the props. We will also be importing forwardRef so we can pass down the ref.

import React, { forwardRef } from 'react'

const Button = forwardRef(
    (
        {
            children,
            type = 'button',
            className,
            variant = 'primary',
            size = 'normal',
            pill,
            disabled = false,
            ...props
        }, ref
    ) => (
        <button
            ref={ref}
            disabled={disabled}
            type={type}
            {...props}
        >
            {children}
        </button>
    ));

export default Button

As we can see from the code above, we already added default states for size, variant, disabled and type.

For styling, we create a classes object where we declare the base styles and all the possible variants and sizes of our component. Then, in our className we join them together to create a composable component.

We create 3 variants for our Button component: primary, secondary and danger. In your design specification, you might have more but we will keep it minimal for this example.

const classes = {
    base: 'focus:outline-none transition ease-in-out duration-300',
    disabled: 'opacity-50 cursor-not-allowed',
    pill: 'rounded-full',
    size: {
        small: 'px-2 py-1 text-sm',
        normal: 'px-4 py-2',
        large: 'px-8 py-3 text-lg'
    },
    variant: {
        primary: 'bg-blue-500 hover:bg-blue-800 focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 text-white',
        secondary: 'bg-gray-200 hover:bg-gray-800 focus:ring-2 focus:ring-gray-500 focus:ring-opacity-50 text-gray-900 hover:text-white',
        danger: 'bg-red-500 hover:bg-red-800 focus:ring-2 focus:ring-red-500 focus:ring-opacity-50 text-white'
    }
}

In our class object we have defined a few keys, base is where our default styling goes, and we have a pill key in case our button is rounded, size for controlling padding and variant for our different button styles.

We need to toggle these classes based on the button prop value, but before we do that we will introduce a new cls function that will clear up all the empty white spaces and format our className before rendering the component. We will add this function to our utils.js file placed under src/utils/.

export const cls = (input) =>
    input
        .replace(/\s+/gm, " ")
        .split(" ")
        .filter((cond) => typeof cond === "string")
        .join(" ")
        .trim();

With this function in our utils folder, we can call our styles from the classes object and render them conditionally based on the prop value. For now, our Button component should look like this:

import React, { forwardRef } from 'react'
import { cls } from '../utils/helpers'

const classes = {
    base: 'focus:outline-none transition ease-in-out duration-300',
    disabled: 'opacity-50 cursor-not-allowed',
    pill: 'rounded-full',
    size: {
        small: 'px-2 py-1 text-sm',
        normal: 'px-4 py-2',
        large: 'px-8 py-3 text-lg'
    },
    variant: {
        primary: 'bg-blue-500 hover:bg-blue-800 focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 text-white',
        secondary: 'bg-gray-200 hover:bg-gray-800 focus:ring-2 focus:ring-gray-500 focus:ring-opacity-50 text-gray-900 hover:text-white',
        danger: 'bg-red-500 hover:bg-red-800 focus:ring-2 focus:ring-red-500 focus:ring-opacity-50 text-white'
    }
}

const Button = forwardRef(
    (
        {
            children,
            type = 'button',
            className,
            variant = 'primary',
            size = 'normal',
            pill,
            disabled = false,
            ...props
        }, ref
    ) => (
        <button
            ref={ref}
            disabled={disabled}
            type={type}
            className={cls(`
                ${classes.base}
                ${classes.size[size]}
                ${classes.variant[variant]}
                ${pill && classes.pill}
                ${disabled && classes.disabled}
                ${className}
            `)}
            {...props}
        >
            {children}
        </button>
    ));

export default Button
React 19 logo

Type checking

To mitigate errors we have to do some prop checking, for UI Components Typescript is the best solution but since we are using plain React we can install the prop-types library for type checking. This will provide runtime errors for our props if an invalid value is given.

Note: Depending on your project specification, you shouldn’t ship PropTypes in production as it’s only used for development.

npm install --save prop-types

Before we export our Button we need to declare the PropTypes based on the data they need to receive, in our case:

Button.propTypes = {
    children: PropTypes.node.isRequired,
    type: PropTypes.oneOf(['submit', 'button']),
    className: PropTypes.string,
    pill: PropTypes.bool,
    disabled: PropTypes.bool,
    variant: PropTypes.oneOf(['primary', 'secondary', 'danger']),
    size: PropTypes.oneOf(['small', 'normal', 'large'])
}

For better component development we would suggest the use of Storybook, as it helps you develop components in isolation but we will not cover that in this tutorial.

With PropTypes added, the final version of our Button component looks like the following:

import React, { forwardRef } from 'react'
import PropTypes from 'prop-types';
import { cls } from '../utils/helpers'

const classes = {
    base: 'focus:outline-none transition ease-in-out duration-300',
    disabled: 'opacity-50 cursor-not-allowed',
    pill: 'rounded-full',
    size: {
        small: 'px-2 py-1 text-sm',
        normal: 'px-4 py-2',
        large: 'px-8 py-3 text-lg'
    },
    variant: {
        primary: 'bg-blue-500 hover:bg-blue-800 focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 text-white',
        secondary: 'bg-gray-200 hover:bg-gray-800 focus:ring-2 focus:ring-gray-500 focus:ring-opacity-50 text-gray-900 hover:text-white',
        danger: 'bg-red-500 hover:bg-red-800 focus:ring-2 focus:ring-red-500 focus:ring-opacity-50 text-white'
    }
}

const Button = forwardRef(
    (
        {
            children,
            type = 'button',
            className,
            variant = 'primary',
            size = 'normal',
            pill,
            disabled = false,
            ...props
        }, ref
    ) => (
        <button
            ref={ref}
            disabled={disabled}
            type={type}
            className={cls(`
                ${classes.base}
                ${classes.size[size]}
                ${classes.variant[variant]}
                ${pill && classes.pill}
                ${disabled && classes.disabled}
                ${className}
            `)}
            {...props}
        >
            {children}
        </button>
    ));

Button.propTypes = {
    children: PropTypes.node.isRequired,
    submit: PropTypes.oneOf(['submit', 'button']),
    className: PropTypes.string,
    pill: PropTypes.bool,
    disabled: PropTypes.bool,
    variant: PropTypes.oneOf(['primary', 'secondary', 'danger']),
    size: PropTypes.oneOf(['small', 'normal', 'large'])
}

export default Button

Conclusion

The basic method we’ve described for creating reusable parts with React and Tailwind CSS is just a small look at the possibilities for making changes. We invite you to use these simple ideas and build on them, try out different kinds of changes, change the size to work in various situations, and add more properties to suit your needs. This adaptable system is made to help you make a group of scalable, flexible parts that can grow with your projects. Use the flexibility of React and Tailwind CSS to make parts that are not just reusable but also carefully adjusted to the special needs of your web apps.

Read a similar post


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

ReactTailwindCSS
Lokman Musliu Founder and CEO of Lucky Media
Lokman Musliu

Founder and CEO of Lucky Media

Stay up-to-date

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

Let’s chat

We partner with a limited number of brands each quarter to ensure senior-level attention on every project.

lokman and arlind headshots
Teamwork