August 4, 2022 · 6 min read · 5,067 views
As you might have noticed already, we are big fans of TailwindCSS. We almost exclusively use it on production sites. That's why we dedicate another post to further contribute for the wonderful Tailwind community. Let's see how we can create small and reusable Tailwind components with React.
Tailwind CSS is renowned for its utility-first approach, offering a robust framework that enables developers to swiftly construct bespoke user interfaces. When integrated with React, this dynamic duo empowers developers to craft powerful and reusable components, streamlining the development process and ensuring consistency across the user interface.
Our design team embraces a component-driven design philosophy, utilizing tools like Figma to assemble the visual elements of our projects. To ensure a seamless transition from design to development, we engage in early collaboration with our design experts. This proactive communication allows us to identify and establish necessary component variants upfront, facilitating a smooth and efficient development workflow that aligns perfectly with the predetermined design specifications.
For this demo we will use Vite to scaffold 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 react
We continue with the installation process by following the framework guide on TailwindCSS documentation:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
This will scaffold 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/forms
And 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.
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.jsx
src/components/form/BaseInput.jsx
src/components/ui/Text.jsx
src/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!
In form-heavy applications or sites, you almost always need reusable input components. They can be very simple or rather complex depending on the initial 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 App
With the BaseInput
component, you can easily add more features such as prepend icons, append, different sizes based on padding, etc.
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 App
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 App
The 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 App
With the card component, we wrap up our series of small and reusable Tailwind components. Throughout this series, we've delved into the intricacies of crafting the Input, Label, Text, and Card components. These foundational elements are integral to our process when we embark on creating marketing websites, applications, or any digital platform.
To get a hands-on experience with everything we've covered, be sure to visit our demo repository, which houses all the code samples from this tutorial. Dive into our demo repo and explore the possibilities these components unlock. Happy coding!
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!