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.tsx
TSX
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>
export const Button = ({ children, ...rest }: ButtonProps) => {
return <button {...rest}>{children}</button>
}
Import the Button
component and use it 👇
App.tsx
TSX
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.tsx
TSX
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.tsx
TSX
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.tsx
TSX
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.tsx
TSX
<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.tsx
TSX
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.tsx
TSX
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.tsx
TSX
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.