Reusable TailwindCSS Button
A reusable button with loading state, icon and hover states!
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.
/ui/Button.tsx
tsximport 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] || ButtonSize.sm
	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="http://www.w3.org/2000/svg"
					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.
Usage
Our button supports 5 sizes and 5 variants along with loading state.
tsximport { Button } from './Button';
// ..rest 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>;