Ultimate Button component (React, TailwindCSS)

Mohanad Alrwaihy

June 23, 2023

54

0

Building components from scratch take a lot of time that's why it is important to have structured way to create components as it may get complicated and hard to maintain with time.

4 min read

Today I will be discussing how we can create a great-looking Button with TailwindCSS and have multiple variances of the button to be used in your application.

Preview

A preview of what kind of button and variance we will create 👇

Prerequisite

Class Variance Authority (CVA)

This library creates variants of any component by providing multiple options for the variance like variant type, size, shape, and any other type of information used to create a unique variant.

Install

POWERSHELL
npm install class-variance-authority

Tailwind Merge

Utility function to efficiently merge Tailwind CSS classes in JS without style conflicts.

Install

POWERSHELL
npm install tailwind-merge

Steps

Basic Button

Let's start first by creating a basic button component and giving it the correct props types so we can send only attributes that are included in the HTML button element 👇

components/ui/Button.tsxTSX
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>

export const Button = ({ children, ...rest }: ButtonProps) => {
  return <button {...rest}>{children}</button>
}

Import the Button component and use it 👇

App.tsxTSX
import { Button } from './components/ui/button'

export default function App(){
	return (
		<Button>Primary</Button>
	)
}

Add forwardRef

Use forwardRef to let the Button component expose a DOM node to the parent component with a ref 👇

components/ui/Button.tsxTSX
import { forwardRef } from 'react'

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ children, ...rest }, ref) => {
    return (
      <button ref={ref} {...rest}>
        {children}
      </button>
    )
  }
)

Now we can use the useRef hook and pass the reference to the Button component if we want 👇

App.tsxTSX
import { Button } from './components/ui/button'
import { useEffect, useRef } from 'react'

export default function App(){
    const buttonRef = useRef<HTMLButtonElement | null>(null)	
      
	useEffect(() => {
	if (buttonRef.current) {
	  buttonRef.current?.focus()
	}
	}, [])

	return (
		<Button ref={buttonRef}>Primary</Button>
	)
}

Add Button Loader & Icon

Buttons can be used sometimes in forms or to fetch data and we need to let the user know if they have to wait for something. We can add a Loader inside the button that we can enable when loading props passed to the button and it's true and in icon to show before the text 👇

components/ui/Button.tsxTSX
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
  loading?: boolean
  icon?: React.ReactNode
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ children, loading, icon, ...rest }, ref) => {
    return (
      <button ref={ref} {...rest}>
        {loading && <ButtonLoader />}
        <span
          className={`inline-flex items-center justify-center gap-2 transition-opacity ${
            loading ? 'opacity-0' : 'opacity-100'
          }`}
        >
          {children}
        </span>
      </button>
    )
  }
)

function ButtonLoader() {
  return (
    <div
      className='absolute inline-flex h-5 w-5 animate-spin items-center rounded-full border-[3px] border-current border-t-transparent text-center leading-6 text-gray-200'
      role='status'
      aria-label='loading'
    >
      <span className='sr-only'>Loading...</span>
    </div>
  )
}

Don't forget to destruct the loading and icon props and add them to the ButtonProps as an optional field.

You can see that I have added a span tag inside the button to render the children and give it some style and a conditional statement to let the children disappear when the loading prop is true. This is necessary so that the width of the button is not changed when the loading state is on.

App.tsxTSX
<Button
  ref={buttonRef}
  icon={<HeartIcon/>}
  loading={true}
  className='bg-purple-600 rounded-xl p-2 flex flex-col items-center justify-center'
>	
  Sign In
</Button>


function HeartIcon() {
  return (
    <svg
    fill='none'
    stroke='currentColor'
    strokeWidth='1.5'
    viewBox='0 0 24 24'
    xmlns='http://www.w3.org/2000/svg'
    aria-hidden='true'
    <path
      strokeLinecap='round'
      strokeLinejoin='round'
      d='M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-   2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z'
> 	  </path>
    </svg>
  )
}

Add Class Variance Authority (CVA)

Let's start with creating the real button with multiple variances!

We need to import cva and type VariantProps from class-variance-authority 👇

POWERSHELL
import { cva, type VariantProps } from 'class-variance-authority'

Create the buttonVariance variable 👇

components/ui/Button.tsxTSX
const buttonVariants = cva(
  // Basic Styles
  '',
  {
    // Type of variants
    variants: {
      // Button colors variant
      variant: {
        primary: '',
        secondary: '',
        destructive: '',
        success: '',
        ghost: '',
        outline: '',
        link: '',
      },
      // Button size variants
      size: {
        sm: '',
        default: '',
        lg: '',
      },
      // Outline variants
      outline: {
        default: '',
        outline: '',
      },
    },
    // The default variant if not specified.
    defaultVariants: { variant: 'primary', size: 'default' },

    // Styles applied when two variants or more are met.
    compoundVariants: [
      {
        variant: 'primary',
        outline: 'outline',
        class: '',
      },
    ],
  }
)

This is a lengthy variable but very clear and straightforward and you can easily understand where to add the classes and how to add new variants and conditions!

Now add the VariantProps to the ButtonProps 👇

TSX
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
  VariantProps<typeof buttonVariants> & {
    loading?: boolean
    icon?: React.ReactNode
  }

Now let's destructure the variants we created which are variant, size, and outline from the button props 👇

TSX
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ children, loading, icon, variant, size, outline, ...rest }, ref) => {
    return (
      <button ref={ref} {...rest}>
        ...
      </button>
    )
  }
)

Add classes with Tailwind Merge (twMerge)

We have everything set now and we need to add the classes and merge with twMerge 👇

components/ui/Button.tsxTSX
import { twMerge } from 'tailwind-merge'

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ children, className, loading, icon, variant, size, outline, ...rest }, ref) => {
    return (
      <button ref={ref}  
      className={twMerge(buttonVariants({ variant, size, outline, className }))} 
      {...rest}
      >
        ...
      </button>
    )
  }
)

Destructure the className from the Button props and then with twMerge we will merge the passed variant props together and add the className at the end to add additional classes to the button or override them.

The only step we have is to add the actual styles to buttonVariants variable 👇

components/ui/Button.tsxTSX
const buttonVariants = cva(
  // Basic Styles
  'relative inline-flex items-center justify-center cursor-pointer rounded-xl tracking-wide shadow hover:shadow-md shadow-white/20 disabled:shadow disabled:cursor-not-allowed outline-none focus-visible:ring-2 ring-offset-4 ring-offset-zinc-900 focus:scale-[0.95] transition border-2 border-white/20',
  {
    // Type of variants
    variants: {
      // Button colors variant
      variant: {
        primary:
          'bg-purple-600 hover:bg-purple-700 disabled:bg-purple-700/60 text-white ring-purple-600',
        secondary:
          'bg-gray-600 hover:bg-gray-700 disabled:bg-gray-700/60 text-white ring-gray-600',
        destructive:
          'bg-pink-800 hover:bg-pink-900 disabled:bg-pink-800/60 text-white ring-pink-800',
        success:
          'bg-emerald-600 hover:bg-emerald-700 disabled:bg-emerald-600/60 text-white ring-emerald-600',
        ghost:
          'bg-transparent shadow-none border-none hover:bg-gray-600 ring-gray-600',
        outline: 'bg-transparent shadow-none hover:bg-gray-700 ring-gray-700',
        link: 'bg-transparent shadow-none border-none text-purple-600',
      },
      // Button size variants
      size: {
        sm: 'py-0.5 px-2 text-xs md:text-sm font-bold',
        default: 'py-2 px-4 text-sm md:text-base font-medium',
        lg: 'py-3.5 px-6 md:text-lg font-medium',
      },
      // Outline variants
      outline: {
        default: '',
        outline: 'bg-transparent',
      },
    },
    // The default variant if not specified.
    defaultVariants: { variant: 'primary', size: 'default' },

    // Styles applied when two variants or more are met.
    compoundVariants: [
      {
        variant: 'primary',
        outline: 'outline',
        class: 'text-purple-600 border-purple-600 hover:text-white',
      },
      {
        variant: 'secondary',
        outline: 'outline',
        class: 'text-gray-200 border-gray-600 hover:text-white',
      },
      {
        variant: 'destructive',
        outline: 'outline',
        class: 'text-pink-600 border-pink-600 hover:text-white',
      },
      {
        variant: 'success',
        outline: 'outline',
        class: 'text-emerald-600 border-emerald-600 hover:text-white',
      },
    ],
  }
)

Examples

It is very easy to choose the variant, size, and outline of the button and also we can add the button styles to other components here are some examples.

Choose Variant

This is how we can use between all the different variants 👇

TSX
// Choose between variants
<Button>Primary</Button> // Default variant (primary)
<Button variant='primary'>Primary</Button>
<Button variant='secondary'>Secondary</Button>
<Button variant='destructive'>Destructive</Button>
<Button variant='success'>Success</Button>
<Button variant='ghost'>Ghost</Button>
<Button variant='outline'>Outline</Button>
<Button variant='link'>Link</Button>

Choose Size

Choosing between sizes is also straightforward 👇

JSX
<Button>Default Size</Button> // Default Size (default)
<Button size='sm'>Small</Button>
<Button size='default'>Default</Button>
<Button size='lg'>Large</Button>

Choose Outline

Adding an outline will make the background of the button transparent and change the border and text colors 👇

TSX
<Button>No Outline</Button> // Default Outline (default)
<Button outline='default'>No Outline</Button>
<Button outline='outlien'>Outline</Button>

Use different tags

The buttonVariants variable can be exported and used with any HTML tag to apply the button style into it but we have to export it first 👇

TSX
// components/ui/Button.tsx
export {Button, buttonVariants}

And now we could use buttonVariants to add styles into a for example 👇

TSX
<a
  href='/'
  className={buttonVariants({ variant: 'primary' })}
>
  A Tag + Primary Button Style
</a>

Conclusion

That's all I hope you have learned something new today and you can apply it in your projects as this method of creating variants can be very useful in also creating other types of components not just buttons with ease because Class Variance Authority (CVA) can be used in also generating text content and have different options because CVA can be viewed as just a neat way of managing string.