Create reusable button Components with React,Typescript , Tailwind and Tailwind-variants
Tags: webdev, beginners, tutorial, react
Published: 2024-01-29 11:34:14

So, You write a few components here and there and have written some button components of your own. But then your button components are not flexible and reusable enough, and you end up rewriting a lot of logic and styles within your react components trying to make it fit different cases in different parts of your application,

Well, you are in the right place. This article serves as an easy-to-follow guide on how to create a reusable and flexible button component using **React, Typescript, Tailwind CSS**, and the **Tailwind-variant** package.

## Prerequisites
As mentioned above, we will be using React, Typecript, and Tailwind CSS when building our components, so basic knowledge of creating components using these will help you better understand how things work together. of course, if u have not used Tailwind CSS before, you are welcome to follow along as Tailwind is straightforward.

## Project Setup
Now let's set things up so we can start building our component. we will start by creating a simple React + Typescript + Tailwind CSS project.

**Create a simple React project with Vite:**

```
npm create vite@latest
```
Follow the prompts to finish the setup process.

**Setup Tailwind in the project:**

Visit the Tailwind website and follow the simple installation guide [here](https://tailwindcss.com/docs/guides/create-react-app)

**Install tailwind-variants:**

```
npm install tailwind-variants
```
**Tailwind Variants** is a first-class variant API library for Tailwind CSS. it helps us create variants of UI components that could be based on a design system easily.

## Creating Button Styles
Now that we are done setting everything up, we can go ahead and start setting the base styles for the button component. Create a components folder, in the src folder of your application. Add a button folder and add 2 files in this folder: a _Button.tsx_ file which is our main button component and a _ButtonStyles.ts_ file which will hold all our button style variants. You should end up with something like this:

![folder structure](https://cdn.hashnode.com/res/hashnode/image/upload/v1704663252021/076be333-3184-4186-a4b1-dc397165b588.png?auto=compress,format&format=webp)

Great. now let's dive into the _ButtonStyles.ts_ components and add some basic button styles.

We are going to create a _**baseButton**_ variant, this variant will contain all the base styles that every button should have. We do this by importing _**tv**_ from tailwind-variants, passing it an object containing our styles, and assigning that to the _**baseButton**_ constant

```TS
import { tv } from 'tailwind-variants';

export const baseButton = tv({
  base: 'text-center relative font-semibold whitespace-nowrap align-middle outline-none inline-flex items-center justify-center select-none',
  variants: {
    size: {
      xs: 'text-xs py-1 px-2',
      md: 'text-sm py-2 px-4',
      lg: 'text-base py-3 px-6',
      xl: 'text-lg py-4 px-8',
      xxl: 'text-xl py-5 px-10',
      square_xs: 'text-xs h-4 w-4 p-1',
      square_sm: 'text-sm h-6 w-6 p-1',
      square_md: 'text-base h-8 w-8 p-1',
      square_lg: 'text-lg h-10 w-10 p-1',
      square_xl: 'text-xl h-12 w-12 p-1',
    },
    vPadding: {
      xs: 'py-[4px]',
      sm: 'py-[8px]',
      md: 'py-[12px]',
      lg: 'py-[16px]',
    },
    vSpace: {
      xs: 'my-1',
      sm: 'my-2',
      md: 'my-4',
      lg: 'my-6',
    },
    HSpace: {
      xs: 'mx-1',
      sm: 'mx-2',
      md: 'mx-4',
      lg: 'mx-6',
    },
    align: {
      center: 'mx-auto',
      right: 'ml-auto',
      left: 'mr-auto',
      top: 'mb-auto',
      bottom: 'mt-auto',
    },
    rounded: {
      none: 'rounded-none',
      xs: 'rounded-[2px]',
      sm: 'rounded-[4px]',
      normal: 'rounded-[8px]',
      lg: 'rounded-[12px]',
      full: 'rounded-full',
    },
    behavior: {
      block: 'w-full',
    },
  },
});

```
Nice!!, we now have some base styles for our buttons. let's go ahead and create a solid button variant with 4 colors: **Green**, **Teal**, **Yellow**, and **Gray**

```TS
// create solid button styles
export const solidButton = tv({
  extend: baseButton,
  variants: {
    color: {
      green:
        'bg-[#58cc02] text-gray-100 shadow active:shadow-none active:translate-y-[5px]',
      teal: 'bg-[#0D9488] text-gray-100 shadow-teal active:shadow-none active:translate-y-[5px]',
      yellow:
        'bg-[#FFC700] text-gray-100 shadow-yellow active:shadow-none active:translate-y-[5px]',
      gray: 'bg-[#64748B] text-gray-100 shadow-blueGray active:shadow-none active:translate-y-[5px]',
    },
  },
});
```
We extend all the variants and styles from the base button and then add custom variants that will apply to solid buttons. this means a solid button will have a color, and size, can be rounded, and have all the other properties from the **_baseButton_**. For the custom shadow, I added new box-shadow variables in my tailwind config.

```TS
/** @type {import('tailwindcss').Config} */
export default {
  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
  theme: {
    boxShadow: {
      DEFAULT: '0 3px 0 #57a300',
      teal: '0 3px 0 #0F766E',
      yellow: '0 3px 0 #E49E00',
      blueGray: '0 3px 0 #475569',
      none: 'none',
    },
    extend: {},
  },
  plugins: [],
};

```
Now, when creating button components, there are several things we need to in mind:

- We want our button to extend the attributes of the native HTML button tag
- We want the variant styles to be applied to our button
- We might want to be able to override button variant styles with custom styles via the **_className_** attribute

With all these in mind, our Button component will look something like this

```TS
/* eslint-disable react/jsx-props-no-spreading */
import { forwardRef, useMemo } from "react";
import { type VariantProps } from "tailwind-variants";
import { TbLoader } from "react-icons/tb";
import { outlineButton, solidButton,ghostButton } from "./ButtonStyles";

// define all the button attributes
type BaseButtonAttributes = React.ComponentPropsWithoutRef<"button">;

// define the ref type
type Ref = HTMLButtonElement;

// extend the base button attributes
interface ButtonProps extends BaseButtonAttributes {
  isLoading?: boolean;
  disabled?: boolean;
  leftIcon?: React.ReactElement;
  rightIcon?: React.ReactElement;
  buttonStyle?: VariantProps<typeof solidButton |typeof outlineButton|typeof ghostButton>;
  className?:string,
  buttonVariant?: "solid" | "outline" | "ghost";
}

const Button = forwardRef<Ref, ButtonProps>((props, ref) => {
  // destructure neccesary props
  const { type, children, buttonStyle, buttonVariant, disabled, isLoading, leftIcon, rightIcon,className, ...rest } = props;

  // determine icon placement
  const { newIcon: icon, iconPlacement } = useMemo(() => {
    let newIcon = rightIcon || leftIcon;

    if (isLoading) {
      newIcon = <TbLoader className="animate-spin" size={25} />;
    }

    return {
      newIcon,
      iconPlacement: rightIcon ? ("right" as const) : ("left" as const),
    };
  }, [isLoading, leftIcon, rightIcon]);

  const renderButtonVariant=()=>{
    if(buttonVariant==="solid"){
      return solidButton({...buttonStyle,className})
    }
   }

  return (
    <button
      className={renderButtonVariant()}
      {...rest}
      type={type ? "submit" : "button"}
      ref={ref}
      disabled={disabled || isLoading}
    >
      {/** render icon before */}
      {icon && iconPlacement === "left" ? (
        <span className={`inline-flex shrink-0 self-center ${children && !isLoading && "mr-2"}`}>{icon}</span>
      ) : null}

      {/** hide button text during loading state */}
      {!isLoading && children}

      {/** render icon after */}
      {icon && iconPlacement === "right" ? (
        <span className={`inline-flex shrink-0 self-center  ${children && !isLoading && "ml-2"}`}>{icon}</span>
      ) : null}
    </button>
  );
});

// set default props
Button.defaultProps = {
  buttonStyle: {},
  buttonVariant: "solid",
  isLoading: false,
  disabled: false,
  leftIcon: undefined,
  rightIcon: undefined,
};

export default Button;

```
that's a lot of code, but don't worry, let's get a closer look at some parts of this Button component.

### Button Props

```TS
// define all the button attributes
type BaseButtonAttributes = React.ComponentPropsWithoutRef<"button">;

// extend the base button attributes
interface ButtonProps extends BaseButtonAttributes {
  isLoading?: boolean;
  disabled?: boolean;
  leftIcon?: React.ReactElement;
  rightIcon?: React.ReactElement;
  buttonStyle?: VariantProps<typeof solidButton>;
  className?:string,
  buttonVariant?: "solid";
}

```
Here, we define the basic props of the component. our **_ButtonProps_** is an interface that extends all the attributes of a button e.g. **_onClick_**, **_ref_**, **_type_**, and more. taking a closer look at this we see the **_buttonStyle_** prop and **_buttonVariant_** prop. The buttonStyle prop is used to set various styles for a button which we defined in the ButtonStyles.ts file e.g. **size**, **color**, **rounded**, and more. Thanks to Typescript, we get amazing intellisence for this when working with our Button.

### Rendering Icons

```TS
  // determine icon placement
  const { newIcon: icon, iconPlacement } = useMemo(() => {
    let newIcon = rightIcon || leftIcon;

    if (isLoading) {
      newIcon = <TbLoader className="animate-spin" size={25} />;
    }

    return {
      newIcon,
      iconPlacement: rightIcon ? ("right" as const) : ("left" as const),
    };
  }, [isLoading, leftIcon, rightIcon]);

```
Here we use the **_useMemo_** hook to run an anonymous arrow function that returns the icon passed to our button and its placement.

Great!!, now we have set up our Button component and we are ready to call it in another component and render some buttons.

## Rendering Button
In my **_App.tsx_** component I import my Button component and call it like so:

```TS
 <div className="p-6 space-x-4 flex-wrap">
        <Button buttonStyle={{ color: 'green', rounded: 'lg', size: 'md' }}>
          Button
        </Button>
        <Button buttonStyle={{ color: 'teal', rounded: 'lg', size: 'md' }}>
          Button
        </Button>
        <Button buttonStyle={{ color: 'yellow', rounded: 'lg', size: 'md' }}>
          Button
        </Button>
        <Button buttonStyle={{ color: 'gray', rounded: 'lg', size: 'md' }}>
          Button
        </Button>

```
**results:**

![buttons](https://cdn.hashnode.com/res/hashnode/image/upload/v1704811611154/962a1e96-ed54-4a4f-8d5d-cb18f32af4b0.png?auto=compress,format&format=webp)

Nice!! , we have rendered 4 buttons with different colors. now let's create more Button variants called an **Outline Button** and **Ghost Button**.

In our **_ButtonStyles.ts_** we will add these button variants

```TS
//create outline button styles
export const outlineButton = tv({
  extend: baseButton,
  base: 'ring-1',
  variants: {
    color: {
      green:
        'ring-[#58cc02] text-[#58cc02] shadow active:shadow-none active:translate-y-[5px]',
      teal: 'ring-[#0D9488] text-[#0D9488] shadow-teal active:shadow-none active:translate-y-[5px]',
      yellow:
        'ring-[#FFC700] text-[#FFC700] shadow-yellow active:shadow-none active:translate-y-[5px]',
      gray: 'ring-[#64748B] text-[#64748B] shadow-blueGray active:shadow-none active:translate-y-[5px]',
    },
  },
});

//create ghost button styles
export const ghostButton = tv({
  extend: baseButton,
  variants: {
    color: {
      green: 'text-[#58cc02]',
      teal: 'text-[#0D9488]',
      yellow: 'text-[#FFC700]',
      gray: 'text-[#64748B]',
    },
  },
});

```
Again, this is very similar to our **Solid Button** variant. But our styles for the various colors have changed. With this, we can make use of these new variants in our **_buttonStyle_** prop, and also modify our buttonVariant prop to have our new variants

```TS
// extend the base button attributes
interface ButtonProps extends BaseButtonAttributes {
  isLoading?: boolean;
  disabled?: boolean;
  leftIcon?: React.ReactElement;
  rightIcon?: React.ReactElement;
  buttonStyle?: VariantProps<typeof solidButton |typeof outlineButton|typeof ghostButton>;
  className?:string,
  buttonVariant?: "solid" | "outline" | "ghost";
}

```
and our **_renderButtonVariant_** function now looks like this:

```TS
 const renderButtonVariant=()=>{
    if(buttonVariant==="solid"){
      return solidButton({...buttonStyle,className})
    }
    if(buttonVariant==="outline"){
      return outlineButton({...buttonStyle,className})
    }
    return ghostButton({...buttonStyle,className})
  }

```
Now let's render some new Button variants in our App.tsx component

```TS
 <div className="p-6 space-x-4">
        <Button
          buttonStyle={{ color: 'green', rounded: 'lg', size: 'md' }}
          buttonVariant="outline"
        >
          Button
        </Button>
        <Button
          buttonStyle={{ color: 'teal', rounded: 'lg', size: 'md' }}
          buttonVariant="outline"
        >
          Button
        </Button>
        <Button
          buttonStyle={{ color: 'yellow', rounded: 'lg', size: 'md' }}
          buttonVariant="outline"
        >
          Button
        </Button>
        <Button
          buttonStyle={{ color: 'gray', rounded: 'lg', size: 'md' }}
          buttonVariant="outline"
        >
          Button
        </Button>
      </div>
 <div className="p-6 space-x-4">
        <Button
          buttonStyle={{ color: 'green', size: 'md' }}
          buttonVariant="ghost"
        >
          Button
        </Button>
        <Button
          buttonStyle={{ color: 'teal', size: 'md' }}
          buttonVariant="ghost"
          leftIcon={<TbAdFilled size={20} />}
        >
          Button
        </Button>
        <Button
          buttonStyle={{ color: 'yellow', size: 'md' }}
          buttonVariant="ghost"
          rightIcon={<TbAdFilled size={20} />}
        >
          Button
        </Button>
        <Button
          buttonStyle={{ color: 'gray', size: 'md' }}
          buttonVariant="ghost"
          rightIcon={<TbAdFilled size={20} />}
        />
      </div>

```
**results:**
![buttons](https://cdn.hashnode.com/res/hashnode/image/upload/v1704812477977/733457b9-702d-487d-b4a8-214887e8ac47.png?auto=compress,format&format=webp)

We can also try out different button states like the **loading state**

```TS
 <Button buttonStyle={{ color: 'green', rounded: 'lg', size: 'md' } isLoading >
  Button
 </Button>

```
**results:**
![loading state](https://cdn.hashnode.com/res/hashnode/image/upload/v1704812644164/90d5238d-6096-4a14-9c3b-c64a466b05e6.png?auto=compress,format&format=webp)

## Overriding Styles
There might be situations where we might need to override our button styles, maybe we need that button just 2px wider or some other edge case has come up and you just need to modify a button without touching the base styles we set. We can easily do that with the **_className_** attribute

```TS
 <div className="flex p-6">
        <Button
          className="px-[100px]"
          buttonStyle={{
            color: 'green',
            rounded: 'lg',
            size: 'md',
            align: 'center',
          }}
        >
          Button
        </Button>
      </div>

```
and the button looks like this
![custom button](https://cdn.hashnode.com/res/hashnode/image/upload/v1704813026751/a68a26b8-79f8-4cfa-ac30-f5edd8a83527.png?auto=compress,format&format=webp)

## Conclusion
**_Tailwind-variants_** provide a flexible approach to building modern components for our Tailwind CSS with Typescript projects. This is great, especially when building design systems using Tailwind CSS.

live code -[code link](https://stackblitz.com/edit/vitejs-vite-gd692j?file=src%2Fcomponents%2Fbutton%2FButton.tsx)