Reusable TailwindCSS Button

A reusable button with loading state, icon and hover states!

Reusable TailwindCSS Button

In modern web, buttons do more than just getting clicked. A lot of times they are required to show loading states, or disabled while form submission.

Not only that, a design system would also mean buttons would have variants and sizes. Fortunately, with TailwindCSS, it is really easy to make button a highly modular component.

This is the button component which I use across my projects. With TS, we get awesome type-safety across our project. We will require clsx which is a tiny (228B) utility for constructing className strings conditionally.

import clsx from 'clsx' interface Props { variant?: 'primary' | 'secondary' | 'white' | 'dark' | 'danger' loading?: boolean size?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' className?: string } const ButtonSize : Record<Props['size'], string> = { xs: 'px-2 py-2 leading-5 text-sm', sm: ' px-3 py-2 leading-5 text-sm', base: 'px-3 py-1 leading-6 text-sm', lg: 'px-4 py-2 text-base', xl: 'px-6 py-3 text-base', } const ButtonVariants = { primary: 'text-white border-indigo-700 bg-indigo-700 hover:bg-indigo-800 hover:border-indigo-800 focus:ring focus:ring-indigo-500 focus:ring-opacity-50 active:bg-indigo-700 active:border-indigo-700', secondary: 'border-pink-200 bg-pink-200 text-pink-700 hover:text-pink-700 hover:bg-pink-300 hover:border-pink-300 focus:ring focus:ring-pink-500 focus:ring-opacity-50 active:bg-pink-200 active:border-pink-200', white: 'border border-gray-300 text-gray-700 bg-white hover:bg-gray-300 focus:outline-none focus:ring-offset-2 focus:ring-brand-500', dark: 'border border-gray-300 dark:border-gray-800 dark:text-gray-100 bg-white shadow-sm dark:bg-gray-700 hover:bg-gray-50 hover:dark:bg-gray-800 focus:outline-none focus:ring focus:ring-gray-500 active:bg-gray-200 active:dark:bg-gray-800', danger: 'text-white bg-red-700 hover:bg-red-800 border border-red-800 focus:outline-none', } export function Button({ children, variant = 'primary', loading = false, size = 'base', rounded, fullWidth, className, ...props }: Props) { const sizeStyles = ButtonSize[size] || const variantStyles = ButtonVariants[variant] || 'primary' return ( <button className={clsx( 'inline-flex justify-center items-center font-medium shadow-sm focus:outline-none', rounded !== 'full' ? sizeStyles : '', variantStyles, rounded === 'full' ? 'rounded-full p-2' : `rounded-${rounded}`, !rounded && 'rounded-md', fullWidth && 'w-full', className )} {...props} > {children} {loading && ( <svg className="w-5 h-5 ml-2 fill-current animate-spin" xmlns="" viewBox="0 0 24 24" > <path fill="none" d="M0 0h24v24H0z" /> <path d="M12 3a9 9 0 0 1 9 9h-2a7 7 0 0 0-7-7V3z" /> </svg> )} </button> ) }

A lot of going on there! Let's see how to use it.


Our button supports 5 sizes and 5 variants along with loading state.

import { Button } from './Button'; // of the code <Button size="lg" variant="primary"> Click Me </Button>; <Button size="md" variant="white" loading={isLoading}> I'm loading and will have a spinner! </Button>;