August 11, 2021 · 5 min read · 28,210 views
This article is one of our most popular blog posts. Given the many advancements in the web development landscape, we believe it’s time for an update. That's why we've refactored the code and offered new techniques for Creating a Reusable Button Component with React, TypeScript, and Tailwind CSS. Feel free to keep reading if you want to compare what we had before with what we suggest now.
TailwindCSS, a utility-first CSS framework, has been our go-to choice for the past three years, and we've successfully utilized it in a multitude of projects. As we scale our web application development, the need for crafting reusable components becomes increasingly apparent. In this overview, we aim to shed light on our methodology for creating a Button component using React in conjunction with TailwindCSS.
While TypeScript offers distinct advantages for building UI components, such as interfaces and enums, and simplifies prop validation, we've chosen to focus on the fundamentals in this tutorial. Hence, we'll be using plain React for clarity and simplicity. However, keep an eye out for a potential TypeScript version of this tutorial, which we may release to address those who prefer the additional type safety and development features TypeScript provides.
The first step in our process is to kick off a new React project. For our development environment, we've opted for Vite, renowned for its speed and efficiency. Let's dive into setting up our project with these powerful tools at our disposal.
npm init vite@latest tw-components --template react
After the required packages have been installed we proceed to install Tailwind by following their official setup guide.
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
We create the config file and PostCSS setup with the command:
npx tailwindcss init -p
Now after we have the tailwind.config.js
file we need to update it in order for the JIT plugin to work correctly. We add mode: 'jit'
and update the purge
key.
module.exports = {
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
darkMode: false,
mode: 'jit', // add this
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}
Last but not least, we update the index.css
file in order to add the Tailwind styles.
@tailwind base;
@tailwind components;
@tailwind utilities;
After importing the three directives, we will cleanup our project a bit, remove the App.css
file, and update the App.jsx
file so we can insert our Button
component inside of it.
import React from 'react'
function App() {
return (
<div className="container mx-auto py-32">
{/* Buttton will be here */}
</div>
)
}
export default App
We create a folder called components
inside the src
directory and add a Button.jsx
file.
We add some base props for our Button but you can extend them depending on the scope, in our case we will be adding children
, type
, className
, variant
, size
, pill
, disabled
and the rest of the props. We will also be importing forwardRef
so we can pass down the ref
.
import React, { forwardRef } from 'react'
const Button = forwardRef(
(
{
children,
type = 'button',
className,
variant = 'primary',
size = 'normal',
pill,
disabled = false,
...props
}, ref
) => (
<button
ref={ref}
disabled={disabled}
type={type}
{...props}
>
{children}
</button>
));
export default Button
As we can see from the code above, we already added default states for size
, variant
, disabled
and type
.
For styling, we create a classes
object where we declare the base styles and all the possible variants and sizes of our component. Then, in our className
we join them together to create a composable component.
We create 3 variants for our Button
component: primary
, secondary
and danger
. In your design specification, you might have more but we will keep it minimal for this example.
const classes = {
base: 'focus:outline-none transition ease-in-out duration-300',
disabled: 'opacity-50 cursor-not-allowed',
pill: 'rounded-full',
size: {
small: 'px-2 py-1 text-sm',
normal: 'px-4 py-2',
large: 'px-8 py-3 text-lg'
},
variant: {
primary: 'bg-blue-500 hover:bg-blue-800 focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 text-white',
secondary: 'bg-gray-200 hover:bg-gray-800 focus:ring-2 focus:ring-gray-500 focus:ring-opacity-50 text-gray-900 hover:text-white',
danger: 'bg-red-500 hover:bg-red-800 focus:ring-2 focus:ring-red-500 focus:ring-opacity-50 text-white'
}
}
Here in our class object we have defined a few keys, base
is where our default styling goes, and we have a pill
key in case our button is rounded, size
for controlling padding and variant
for our different button styles.
Now we need to toggle these classes based on the button prop value, but before we do that we will introduce a new cls
function that will clear up all the empty white-spaces and format our className
before rendering the component. We will add this function to our utils.js
file placed under src/utils/
.
export const cls = (input) =>
input
.replace(/\s+/gm, " ")
.split(" ")
.filter((cond) => typeof cond === "string")
.join(" ")
.trim();
Now with this function in our utils
folder, we can call our styles from the classes object and render them conditionally based on the prop value. For now, our Button
component should look like this:
import React, { forwardRef } from 'react'
import { cls } from '../utils/helpers'
const classes = {
base: 'focus:outline-none transition ease-in-out duration-300',
disabled: 'opacity-50 cursor-not-allowed',
pill: 'rounded-full',
size: {
small: 'px-2 py-1 text-sm',
normal: 'px-4 py-2',
large: 'px-8 py-3 text-lg'
},
variant: {
primary: 'bg-blue-500 hover:bg-blue-800 focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 text-white',
secondary: 'bg-gray-200 hover:bg-gray-800 focus:ring-2 focus:ring-gray-500 focus:ring-opacity-50 text-gray-900 hover:text-white',
danger: 'bg-red-500 hover:bg-red-800 focus:ring-2 focus:ring-red-500 focus:ring-opacity-50 text-white'
}
}
const Button = forwardRef(
(
{
children,
type = 'button',
className,
variant = 'primary',
size = 'normal',
pill,
disabled = false,
...props
}, ref
) => (
<button
ref={ref}
disabled={disabled}
type={type}
className={cls(`
${classes.base}
${classes.size[size]}
${classes.variant[variant]}
${pill && classes.pill}
${disabled && classes.disabled}
${className}
`)}
{...props}
>
{children}
</button>
));
export default Button
Now in order to mitigate errors we have to do some prop checking, for UI Components Typescript is the best solution but since we are using plain React we can install the prop-types
library for type checking. This will provide runtime errors for our props if an invalid value is given.
Note: Depending on your project specification, you shouldn't ship PropTypes in production as it's only used for development.
npm install --save prop-types
Before we export our Button we need to declare the PropTypes based on the data they need to receive, in our case:
Button.propTypes = {
children: PropTypes.node.isRequired,
type: PropTypes.oneOf(['submit', 'button']),
className: PropTypes.string,
pill: PropTypes.bool,
disabled: PropTypes.bool,
variant: PropTypes.oneOf(['primary', 'secondary', 'danger']),
size: PropTypes.oneOf(['small', 'normal', 'large'])
}
For better component development we would suggest the use of Storybook, as it helps you develop components in isolation but we will not cover that in this tutorial.
With PropTypes added, the final version of our Button component looks like the following:
import React, { forwardRef } from 'react'
import PropTypes from 'prop-types';
import { cls } from '../utils/helpers'
const classes = {
base: 'focus:outline-none transition ease-in-out duration-300',
disabled: 'opacity-50 cursor-not-allowed',
pill: 'rounded-full',
size: {
small: 'px-2 py-1 text-sm',
normal: 'px-4 py-2',
large: 'px-8 py-3 text-lg'
},
variant: {
primary: 'bg-blue-500 hover:bg-blue-800 focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 text-white',
secondary: 'bg-gray-200 hover:bg-gray-800 focus:ring-2 focus:ring-gray-500 focus:ring-opacity-50 text-gray-900 hover:text-white',
danger: 'bg-red-500 hover:bg-red-800 focus:ring-2 focus:ring-red-500 focus:ring-opacity-50 text-white'
}
}
const Button = forwardRef(
(
{
children,
type = 'button',
className,
variant = 'primary',
size = 'normal',
pill,
disabled = false,
...props
}, ref
) => (
<button
ref={ref}
disabled={disabled}
type={type}
className={cls(`
${classes.base}
${classes.size[size]}
${classes.variant[variant]}
${pill && classes.pill}
${disabled && classes.disabled}
${className}
`)}
{...props}
>
{children}
</button>
));
Button.propTypes = {
children: PropTypes.node.isRequired,
submit: PropTypes.oneOf(['submit', 'button']),
className: PropTypes.string,
pill: PropTypes.bool,
disabled: PropTypes.bool,
variant: PropTypes.oneOf(['primary', 'secondary', 'danger']),
size: PropTypes.oneOf(['small', 'normal', 'large'])
}
export default Button
The foundational approach we've outlined for building reusable components with React and Tailwind CSS offers just a glimpse into the potential for customization. You're encouraged to take these basics and extend them further, experiment with adding a diverse array of variants, adjust sizing to fit different contexts, and introduce additional props to meet your specific needs. This flexible framework is designed to empower you to create a suite of scalable, adaptable components that can evolve alongside your projects. Embrace the versatility of React and Tailwind CSS to craft components that are not only reusable but also finely tuned to the unique demands of your web applications.
Read a similar post
If you need help with a Laravel project let's get in touch.
Lucky Media is proud to be recognized as a Top Next.js Development Agency
Technologies:
Related Posts
Stay up to date
Be updated with all news, products and tips we share!