Tailwind with React & Vue
Tailwind with React & Vue
Learn how to effectively use Tailwind CSS with modern JavaScript frameworks like React and Vue. Understand component patterns, conditional styling, and framework-specific best practices.
Setting Up Tailwind with React
Tailwind integrates seamlessly with React projects. Here's how to set it up in a Create React App or Vite project:
Installation in React Project
# Install Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
# Generate configuration files
npx tailwindcss init -p
# In tailwind.config.js
module.exports = {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
# In src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
className instead of class because class is a reserved keyword in JavaScript. This is the most common mistake when starting with React and Tailwind.
Using className in React Components
React uses JSX, which requires the className prop for CSS classes:
Basic React Component with Tailwind
import React from 'react';
function Button({ children, variant = 'primary' }) {
const baseClasses = 'px-4 py-2 rounded-lg font-semibold transition-colors';
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
danger: 'bg-red-600 text-white hover:bg-red-700',
};
return (
<button className={`${baseClasses} ${variants[variant]}`}>
{children}
</button>
);
}
export default Button;
Conditional Classes with clsx and classnames
Managing conditional classes with string concatenation can become messy. Libraries like clsx or classnames make this much cleaner:
Installing and Using clsx
# Install clsx
npm install clsx
// Using clsx in a component
import clsx from 'clsx';
function Alert({ message, type, dismissible }) {
return (
<div className={clsx(
'p-4 rounded-lg border',
{
'bg-blue-50 border-blue-200 text-blue-800': type === 'info',
'bg-green-50 border-green-200 text-green-800': type === 'success',
'bg-yellow-50 border-yellow-200 text-yellow-800': type === 'warning',
'bg-red-50 border-red-200 text-red-800': type === 'error',
'pr-12': dismissible,
}
)}>
{message}
</div>
);
}
clsx library for cleaner conditional class logic. It handles arrays, objects, and conditional expressions elegantly, making your component code much more readable.
The cn() Helper Function Pattern
Many modern React projects use a cn() helper that combines clsx with tailwind-merge to properly handle class conflicts:
Creating the cn() Utility
# Install dependencies
npm install clsx tailwind-merge
// lib/utils.js
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs) {
return twMerge(clsx(inputs));
}
// Usage in component
import { cn } from '@/lib/utils';
function Card({ className, children, ...props }) {
return (
<div
className={cn(
'rounded-lg border bg-white p-6 shadow-sm',
className
)}
{...props}
>
{children}
</div>
);
}
// Now you can override default classes
<Card className="bg-blue-50 p-8">
Content
</Card>
className="p-8" to override p-6, both classes apply and the last one in the CSS file wins (unpredictable). tailwind-merge intelligently removes conflicting classes.
React Component Patterns with Tailwind
Here are common patterns for building reusable components:
Variant-Based Button Component
import { cn } from '@/lib/utils';
const buttonVariants = {
variant: {
default: 'bg-blue-600 text-white hover:bg-blue-700',
destructive: 'bg-red-600 text-white hover:bg-red-700',
outline: 'border border-gray-300 bg-transparent hover:bg-gray-100',
ghost: 'hover:bg-gray-100',
link: 'text-blue-600 underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
};
function Button({
className,
variant = 'default',
size = 'default',
children,
...props
}) {
return (
<button
className={cn(
'inline-flex items-center justify-center rounded-md font-medium',
'transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2',
'disabled:pointer-events-none disabled:opacity-50',
buttonVariants.variant[variant],
buttonVariants.size[size],
className
)}
{...props}
>
{children}
</button>
);
}
State-Based Styling in React
React's state can drive your Tailwind classes:
Interactive Accordion Component
import { useState } from 'react';
import { ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
function Accordion({ title, children }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="border rounded-lg overflow-hidden">
<button
onClick={() => setIsOpen(!isOpen)}
className={cn(
'w-full px-4 py-3 flex items-center justify-between',
'text-left font-medium transition-colors',
isOpen ? 'bg-blue-50' : 'bg-white hover:bg-gray-50'
)}
>
<span>{title}</span>
<ChevronDown
className={cn(
'w-5 h-5 transition-transform duration-200',
isOpen && 'rotate-180'
)}
/>
</button>
<div
className={cn(
'px-4 overflow-hidden transition-all duration-200',
isOpen ? 'py-3 max-h-96' : 'max-h-0'
)}
>
{children}
</div>
</div>
);
}
Setting Up Tailwind with Vue 3
Vue 3 works beautifully with Tailwind CSS. Here's the setup process:
Installation in Vue Project
# Create Vue project with Vite
npm create vite@latest my-vue-app -- --template vue
# Install Tailwind
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
# Configure tailwind.config.js
module.exports = {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
# In src/style.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Class Binding in Vue
Vue offers powerful class binding syntax that works perfectly with Tailwind:
Vue Class Binding Patterns
<template>
<!-- String binding -->
<div :class="classes">Content</div>
<!-- Object syntax -->
<button
:class="{
'bg-blue-600 text-white': variant === 'primary',
'bg-gray-200 text-gray-800': variant === 'secondary',
'opacity-50 cursor-not-allowed': disabled,
}"
>
Click me
</button>
<!-- Array syntax -->
<div :class="[baseClasses, variantClasses, sizeClasses]">
Content
</div>
<!-- Mixed syntax -->
<div :class="[
'rounded-lg border',
{ 'bg-blue-50': isActive },
isLarge ? 'p-8' : 'p-4'
]">
Content
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const variant = ref('primary');
const disabled = ref(false);
const isActive = ref(true);
const isLarge = ref(false);
const classes = computed(() => {
return `px-4 py-2 rounded ${variant.value === 'primary' ? 'bg-blue-600' : 'bg-gray-200'}`;
});
</script>
clsx. The object and array syntax handle conditional classes elegantly out of the box.
Vue 3 Component with Tailwind
Here's a complete Vue 3 component using Composition API:
Reusable Badge Component
<template>
<span
:class="[
'inline-flex items-center rounded-full px-3 py-1 text-sm font-medium',
variantClasses,
sizeClasses,
className
]"
>
<slot name="icon" />
<slot />
</span>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({
variant: {
type: String,
default: 'default',
validator: (value) => ['default', 'success', 'warning', 'error'].includes(value)
},
size: {
type: String,
default: 'md',
validator: (value) => ['sm', 'md', 'lg'].includes(value)
},
className: {
type: String,
default: ''
}
});
const variantClasses = computed(() => {
const variants = {
default: 'bg-gray-100 text-gray-800',
success: 'bg-green-100 text-green-800',
warning: 'bg-yellow-100 text-yellow-800',
error: 'bg-red-100 text-red-800',
};
return variants[props.variant];
});
const sizeClasses = computed(() => {
const sizes = {
sm: 'text-xs px-2 py-0.5',
md: 'text-sm px-3 py-1',
lg: 'text-base px-4 py-1.5',
};
return sizes[props.size];
});
</script>
Vue 3 With Dynamic Styling
Leverage Vue's reactivity with Tailwind classes:
Interactive Card Component
<template>
<div
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
:class="cardClasses"
>
<div
:class="[
'absolute inset-0 bg-gradient-to-br transition-opacity duration-300',
'from-blue-500/20 to-purple-500/20',
isHovered ? 'opacity-100' : 'opacity-0'
]"
></div>
<div class="relative z-10">
<h3 class="text-xl font-bold mb-2">{{ title }}</h3>
<p class="text-gray-600">{{ description }}</p>
<button
@click="toggleFavorite"
:class="buttonClasses"
>
{{ isFavorite ? '❤️' : '🤍' }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const props = defineProps({
title: String,
description: String
});
const isHovered = ref(false);
const isFavorite = ref(false);
const cardClasses = computed(() => [
'relative overflow-hidden rounded-xl border p-6 transition-all duration-300',
isHovered.value
? 'shadow-xl scale-105 border-blue-300'
: 'shadow-md border-gray-200'
]);
const buttonClasses = computed(() => [
'mt-4 px-4 py-2 rounded-lg transition-colors',
isFavorite.value
? 'bg-red-100 text-red-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
]);
const toggleFavorite = () => {
isFavorite.value = !isFavorite.value;
};
</script>
Component Composition Patterns
Both React and Vue benefit from composition patterns with Tailwind:
React Compound Components
// Card.jsx
function Card({ children, className }) {
return (
<div className={cn('rounded-lg border bg-white shadow-sm', className)}>
{children}
</div>
);
}
Card.Header = function CardHeader({ children, className }) {
return (
<div className={cn('px-6 py-4 border-b', className)}>
{children}
</div>
);
};
Card.Body = function CardBody({ children, className }) {
return (
<div className={cn('px-6 py-4', className)}>
{children}
</div>
);
};
Card.Footer = function CardFooter({ children, className }) {
return (
<div className={cn('px-6 py-4 border-t bg-gray-50', className)}>
{children}
</div>
);
};
// Usage
<Card>
<Card.Header>
<h3 className="text-lg font-semibold">Title</h3>
</Card.Header>
<Card.Body>
<p>Content goes here</p>
</Card.Body>
<Card.Footer>
<button className="btn-primary">Action</button>
</Card.Footer>
</Card>
Headless UI Libraries
Headless UI libraries provide accessible components without styling, perfect for Tailwind:
Headless UI with React
npm install @headlessui/react
import { Menu, Transition } from '@headlessui/react';
import { Fragment } from 'react';
function Dropdown() {
return (
<Menu as="div" className="relative inline-block text-left">
<Menu.Button className="inline-flex justify-center w-full px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700">
Options
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 w-56 mt-2 origin-top-right bg-white divide-y divide-gray-100 rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="px-1 py-1">
<Menu.Item>
{({ active }) => (
<button
className={`${
active ? 'bg-blue-500 text-white' : 'text-gray-900'
} group flex rounded-md items-center w-full px-2 py-2 text-sm`}
>
Edit
</button>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
);
}
className={`text-${color}-500`}. Tailwind's purge process can't detect these and they won't work. Always use complete class names or define a mapping object.
Exercise 1: React Button Component
Create a reusable React button component with:
- Three variants: primary, secondary, outline
- Three sizes: sm, md, lg
- Loading state with spinner
- Disabled state
- Use clsx or cn() helper
Exercise 2: Vue Input Component
Build a Vue 3 input component with Tailwind that includes:
- Label and error message support
- Different states: default, focus, error, disabled
- Optional icon slot
- Computed classes based on props
- v-model support
Exercise 3: Framework-Agnostic Card Grid
Create a responsive card grid component in either React or Vue:
- Grid layout: 1 column on mobile, 2 on tablet, 3 on desktop
- Cards with hover effects
- Image, title, description, and action button
- Favorite toggle functionality
- Use composition pattern (Card.Header, Card.Body, etc.)