Small and reusable Tailwind components with React

August 4, 2022 · 6 min read

As you might have noticed already, we are big fans of TailwindCSS. That’s why we’re writing another post to help the great Tailwind community. Let’s look at how to make small, reusable Tailwind components with React.
Tailwind CSS is known for its utility-first approach, offering a strong framework for developers to quickly build custom user interfaces. When used with React, these two together let developers create powerful, reusable components. This makes developing faster and keeps the user interface consistent.
Our design team believes in a design approach focused on components, using tools like Figma to put together the visual parts of our projects. To move smoothly from design to development, we work closely with our design experts early on. This early teamwork helps us spot and set up the needed component variations from the start, leading to a development process that’s efficient and matches the design plans well.
Installation Process
For this demo we will use Vite to create a new React application. We start by running the following command in our terminal:
npm create vite@latest --template react
# npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template reactWe continue with the installation process by following the framework guide on TailwindCSS documentation:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -pThis will create two new files: tailwind.config.cjs and postcss.config.cjs
Our Tailwind config looks like this:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};Finally, to load the CSS, we need to add the following lines to our index.css file:
@tailwind base;
@tailwind components;
@tailwind utilities;When working with Tailwind components we highly recommend the @tailwindcss/forms as it provides a basic reset for form styles and makes styling a bit easier.
You can install it with:
npm install -D @tailwindcss/formsAnd include it in the plugin array inside the Tailwind config.
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [
require("@tailwindcss/forms"),
],
};We are now ready to work on our components.
Directory Structure
For better file structure we recommend creating a components directory and splitting the components based on functionality ( form, ui, etc ).
For example:
src/components/form/Button.jsxsrc/components/form/BaseInput.jsxsrc/components/ui/Text.jsxsrc/components/ui/Card.jsx
We cover creating a reusable button in an earlier tutorial.
Note that for all examples we use the clsx package for constructing className strings conditionally. Make sure you have it installed if you follow this tutorial.
Another note: We are aware that Typescript is better for these examples, but to keep this tutorial beginner friendly we will focus only on React. We might add Typescript examples in the future. Thanks!
Reusable Input Components
In applications or websites with a lot of forms, you often need reusable input components. They can be very simple or quite complex, depending on the design.
When working with Tailwind and React, we like to have a BaseInput component that can be easily extended and supports most of the common features such as:
Support for multiple states, for example, error state, valid state ( passes validation ), disabled, etc.
Support for multiple styling options, sometimes we might have inputs with different borders (rounded, square) depending on the use case.
Option to add a label with the correct id
We like to start with our Label component which we call BaseLabel, and it’s the smallest component that only takes an id, className, and children.
Why a separate component? Well, you might use the same label for other form elements such as select, radio, checkboxes, etc.
This is our finished BaseLabel component:
import { clsx } from "clsx"
const BaseLabel = ({ id, className, children }) => {
return (
<label
className={clsx('block mb-2 text-base font-medium text-gray-700', className)}
htmlFor={id}
>
{children}
</label>
)
}
export default BaseLabel;Now that we have our BaseLabel component we can conditionally include it in our BaseInput component if the user adds a label.
For our BaseInput we need the following props: id, label, type, error, required, disabled, valid, className, errorText, and rounded.
To make everything easy to handle, we always create a styles object with all the required classes. Thus, we can conditionally include them based on prop value.
In our example, we use React v18 so we can benefit from the new useId hook to automatically generate ids for inputs. If you are using an older React version you can always use a package like ReachUI Auto id.
Here is our finished BaseInput component:
import { useId } from 'react'
import { clsx } from 'clsx'
import BaseLabel from './BaseLabel'
const BaseInput = (props) => {
const {
label,
type = 'text',
error = false,
required = false,
disabled = false,
valid = false,
className = '',
errorText = '',
rounded = 'lg',
...rest
} = props;
const id = useId();
const styles = {
base: 'border-transparent flex-1 appearance-none border w-full py-2 px-4 bg-white text-gray-700 shadow-sm text-base focus:outline-none focus:ring-2 focus:border-transparent',
state: {
normal: 'placeholder-gray-400 border-gray-300 focus:ring-purple-600',
error: 'border-red-600 focus:ring-red-600',
valid: 'border-green-600 focus:ring-green-600',
disabled: 'cursor-not-allowed bg-gray-100 shadow-inner text-gray-400'
},
rounded: {
none: null,
sm: 'rounded-sm',
md: 'rounded-md',
lg: 'rounded-lg',
}
}
return (
<div className={clsx('relative', className)}>
{label && (
<BaseLabel id={id}>
{label} {required && '*'}
</BaseLabel>
)}
<input
id={id}
type={type}
className={clsx([styles.base,
rounded && styles.rounded[rounded],
error ? styles.state.error : styles.state.normal,
valid ? styles.state.valid : styles.state.normal,
disabled && styles.state.disabled
])}
disabled={disabled}
required={required}
{...rest}
/>
{error && <p className="mt-2 text-sm text-red-600">{errorText}</p>}
</div>
)
}
export default BaseInput;This is what the component looks like with some of the possible variations:

And below you have the example code used in the image above:
import BaseInput from './components/form/BaseInput'
function App() {
return (
<div className="bg-white min-h-screen">
<div className="max-w-4xl mx-auto py-20">
<div className="grid grid-cols-4 gap-8">
<BaseInput label="Your name" required />
<BaseInput
label="Your name"
placeholder="Error State"
errorText="Field is required"
error
required
/>
<BaseInput label="Your name" placeholder="Success State" valid />
<BaseInput label="Your name" placeholder="Disabled" disabled />
<BaseInput label="Your name" placeholder="Square" rounded="none" />
<BaseInput label="Your name" placeholder="Small" rounded="sm" />
<BaseInput label="Your name" placeholder="Medium" rounded="md" />
<BaseInput label="Your name" placeholder="Large is default" />
</div>
</div>
</div>
)
}
export default AppWith the BaseInput component, you can easily add more features such as prepend icons, append, different sizes based on padding, etc.
Reusable Text Component
When working with content-heavy marketing sites using Next.js or React, a Text component is something that we constantly use.
We can have different sizes, variants, or even different tags ( for ex: p, span, h2, h3, etc )
Let’s construct the component:
import { clsx } from 'clsx';
const Text = ({ size = 'base', variant = 'gray', as = 'p', className, children }) => {
const Tag = as;
const sizes = {
sm: 'font-medium text-sm leading-normal',
base: 'font-medium text-base leading-normal',
lg: 'font-semibold text-lg md:text-2xl leading-relaxed'
}
const variants = {
gray: 'text-gray-600',
white: 'text-white',
dark: 'text-gray-900'
}
return (
<Tag className={clsx(sizes[size], variants[variant], className)}>
{children}
</Tag>
)
}
export default Text;A simple example may be the following:
import Text from "./components/ui/Text"
function App() {
return (
<div className="bg-white min-h-screen">
<div className="container mx-auto py-20">
<div className="space-y-3">
<Text size="sm">This is small text</Text>
<Text>This is normal text</Text>
<Text size="lg">This is large text</Text>
<Text size="lg" variant="dark">This is large and dark text</Text>
</div>
</div>
</div>
)
}
export default AppReusable Card Component
Cards are one of the most used UI components in applications ( at least in our experience ). We always try to make them as reusable as possible.
Here is an example Card component that we split into 4 parts: The Card wrapper, Card Header (with or without Title), Card Body, and Card Footer.
import { clsx } from 'clsx';
const Card = ({ className, children }) => {
return (
<div className={clsx('overflow-hidden bg-white rounded-lg shadow', className)}>
{children}
</div>
)
}
const Header = ({ className, children }) => {
return (
<div className={clsx('px-4 py-5 bg-white border-b border-gray-200 sm:px-6', className)}>
{children}
</div>
)
}
const HeaderTitle = ({ className, as = 'h3', children }) => {
const Tag = as;
return (
<Tag className={clsx('text-lg font-medium leading-6 text-gray-900', className)}>
{children}
</Tag>
)
}
const Body = ({ className, children }) => {
return (
<div className={clsx('px-4 py-5 sm:p-6', className)}>
{children}
</div>
)
}
const Footer = ({ className, children }) => {
return (
<div className={clsx('bg-white border-t border-gray-200', className)}>
{children}
</div>
)
}
Card.Header = Header;
Card.Header.Title = HeaderTitle;
Card.Body = Body;
Card.Footer = Footer;
export default Card;We can have multiple variations of our Card component. Let’s see an example:
import Card from "./components/ui/Card"
function App() {
return (
<div className="bg-gray-100 min-h-screen">
<div className="container mx-auto py-20">
<div className="grid grid-cols-4 gap-4">
<Card>
<Card.Header>
<Card.Header.Title>Card Title</Card.Header.Title>
</Card.Header>
<Card.Body>
This is the body
</Card.Body>
<Card.Footer>
This is the footer
</Card.Footer>
</Card>
<Card>
<Card.Header>
<Card.Header.Title>Card Title</Card.Header.Title>
</Card.Header>
<Card.Body>
Card with body and header/title
</Card.Body>
</Card>
<Card>
<Card.Body>
Card with body only
</Card.Body>
</Card>
<Card>
Card without body
</Card>
</div>
</div>
</div>
)
}
export default AppThe image below shows some the possible variations with this Card component:

With the current setup, we have the option to add className to every Card item. This allows us to further customize if needed, so we can keep everything small and composable.
You can also extend the component if you have multiple variations. For example, let’s add the option to include different footer variants and sizes to the Card Footer.
const Footer = ({ variant = 'white', size = 'md', className, children }) => {
const styles = {
sizes: {
sm: 'px-1 py-5 sm:px-2',
md: 'px-4 py-5 sm:px-6',
lg: 'px-4 py-8 sm:px-8',
},
variants: {
white: 'bg-white border-t border-gray-200',
dark: 'bg-gray-900 border-t border-gray-700',
gray: 'bg-gray-50',
}
}
return (
<div className={clsx([
styles.variants[variant],
styles.sizes[size],
className
])}>
{children}
</div>
)
}To keep things simple, we will only display the modified code for the Card Footer. With small changes, we may have multiple footer variants.
In your code you can use them like so:
import Card from "./components/Card"
function App() {
return (
<div className="bg-gray-100 min-h-screen">
<div className="container mx-auto py-20">
<div className="grid grid-cols-4 gap-4">
<Card>
<Card.Body>
Card with gray footer.
</Card.Body>
<Card.Footer variant="gray">
This is the gray footer
</Card.Footer>
</Card>
<Card>
<Card.Body>
Card with dark footer
</Card.Body>
<Card.Footer size="sm" variant="dark">
<p className="text-white">
This is the small dark footer
</p>
</Card.Footer>
</Card>
</div>
</div>
</div>
)
}
export default AppConclusion
With the card component, we finish our series on small, reusable Tailwind components. We’ve looked closely at making the Input, Label, Text, and Card components. These basic parts are key to our work when we start making marketing websites, apps, or any digital platform.
To understand what we’ve talked about, make sure to check out our demo repo, which has all the code examples from this tutorial.
Bring Your Ideas to Life 🚀
If you need help with a React project let’s get in touch.
Lucky Media is proud to be recognized as a leading Next js Development Agency
Technologies

Stay up-to-date
Be updated with all news, products and tips we share!

