Master integrating Tailwind CSS with popular JavaScript frameworks including React, Next.js, Vue.js, Svelte, and more. Learn framework-specific patterns, best practices, and optimization techniques.
Tailwind CSS integrates seamlessly with all major JavaScript frameworks, but each framework has its own patterns, conventions, and best practices. Whether you're using React, Vue, Svelte, or any other framework, understanding how to properly integrate and optimize Tailwind will unlock its full potential.
In this lesson, you'll learn framework-specific integration techniques, discover patterns for building reusable components, and master the art of combining utility-first CSS with component-based architectures.
Component Reusability: Build once, use everywhere
Type Safety: TypeScript integration for props and styles
Performance: Framework-specific optimizations
Developer Experience: Intellisense, autocomplete, and linting
Let's explore how to integrate Tailwind CSS with the most popular frameworks and build production-ready applications.
React is one of the most popular frameworks for Tailwind CSS. Let's explore integration patterns and best practices.
Create React App:
1# Create new React app
2npx create-react-app my-app
3cd my-app
4
5# Install Tailwind CSS
6npm install -D tailwindcss postcss autoprefixer
7npx tailwindcss init -pConfiguration:
1// tailwind.config.js
2/** @type {import('tailwindcss').Config} */
3module.exports = {
4 content: [
5 "./src/**/*.{js,jsx,ts,tsx}",
6 ],
7 theme: {
8 extend: {},
9 },
10 plugins: [],
11}1/* src/index.css */
2@tailwind base;
3@tailwind components;
4@tailwind utilities;1// src/index.js
2import React from 'react';
3import ReactDOM from 'react-dom/client';
4import './index.css'; // Import Tailwind CSS
5import App from './App';
6
7const root = ReactDOM.createRoot(document.getElementById('root'));
8root.render(<App />);Basic Component:
1// components/Button.jsx
2export function Button({ children, variant = 'primary', ...props }) {
3 const baseClasses = 'px-4 py-2 rounded-lg font-semibold transition-colors';
4
5 const variantClasses = {
6 primary: 'bg-blue-500 hover:bg-blue-600 text-white',
7 secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
8 outline: 'border-2 border-blue-500 text-blue-500 hover:bg-blue-50',
9 };
10
11 return (
12 <button
13 className={`${baseClasses} ${variantClasses[variant]}`}
14 {...props}
15 >
16 {children}
17 </button>
18 );
19}Using the Component:
1import { Button } from './components/Button';
2
3function App() {
4 return (
5 <div className="container mx-auto p-4">
6 <Button variant="primary">Primary Button</Button>
7 <Button variant="secondary">Secondary Button</Button>
8 <Button variant="outline">Outline Button</Button>
9 </div>
10 );
11}Using Template Literals:
1function Alert({ type, message }) {
2 return (
3 <div className={`p-4 rounded-lg ${
4 type === 'success' ? 'bg-green-100 text-green-900' :
5 type === 'error' ? 'bg-red-100 text-red-900' :
6 'bg-blue-100 text-blue-900'
7 }`}>
8 {message}
9 </div>
10 );
11}Using clsx or classnames:
1npm install clsx1import clsx from 'clsx';
2
3function Button({ children, primary, disabled }) {
4 return (
5 <button
6 className={clsx(
7 'px-4 py-2 rounded-lg font-semibold',
8 primary ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-900',
9 disabled && 'opacity-50 cursor-not-allowed'
10 )}
11 disabled={disabled}
12 >
13 {children}
14 </button>
15 );
16}1# Install TypeScript
2npm install -D typescript @types/react @types/react-dom1// components/Button.tsx
2import React from 'react';
3
4interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
5 variant?: 'primary' | 'secondary' | 'outline';
6 children: React.ReactNode;
7}
8
9export function Button({ children, variant = 'primary', className, ...props }: ButtonProps) {
10 const baseClasses = 'px-4 py-2 rounded-lg font-semibold transition-colors';
11
12 const variantClasses = {
13 primary: 'bg-blue-500 hover:bg-blue-600 text-white',
14 secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
15 outline: 'border-2 border-blue-500 text-blue-500 hover:bg-blue-50',
16 };
17
18 return (
19 <button
20 className={`${baseClasses} ${variantClasses[variant]} ${className || ''}`}
21 {...props}
22 >
23 {children}
24 </button>
25 );
26}Card Component:
1interface CardProps {
2 title: string;
3 description: string;
4 image?: string;
5 footer?: React.ReactNode;
6}
7
8export function Card({ title, description, image, footer }: CardProps) {
9 return (
10 <div className="bg-white rounded-lg shadow-md overflow-hidden">
11 {image && (
12 <img src={image} alt={title} className="w-full h-48 object-cover" />
13 )}
14 <div className="p-6">
15 <h3 className="text-xl font-bold text-gray-900 mb-2">{title}</h3>
16 <p className="text-gray-600">{description}</p>
17 </div>
18 {footer && (
19 <div className="bg-gray-50 px-6 py-4 border-t border-gray-200">
20 {footer}
21 </div>
22 )}
23 </div>
24 );
25}Usage:
1<Card
2 title="Product Name"
3 description="Product description goes here"
4 image="product.jpg"
5 footer={
6 <button className="bg-blue-500 text-white px-4 py-2 rounded">
7 Buy Now
8 </button>
9 }
10/>Next.js has first-class support for Tailwind CSS with built-in PostCSS processing.
1# Create new Next.js app with Tailwind
2npx create-next-app@latest my-app --typescript --tailwind
3
4# Or add to existing project
5npm install -D tailwindcss postcss autoprefixer
6npx tailwindcss init -p1// tailwind.config.js
2/** @type {import('tailwindcss').Config} */
3module.exports = {
4 content: [
5 './pages/**/*.{js,ts,jsx,tsx,mdx}',
6 './components/**/*.{js,ts,jsx,tsx,mdx}',
7 './app/**/*.{js,ts,jsx,tsx,mdx}',
8 ],
9 theme: {
10 extend: {},
11 },
12 plugins: [],
13}1/* app/globals.css or styles/globals.css */
2@tailwind base;
3@tailwind components;
4@tailwind utilities;Layout Component:
1// app/layout.tsx
2import './globals.css';
3import type { Metadata } from 'next';
4
5export const metadata: Metadata = {
6 title: 'My App',
7 description: 'Built with Next.js and Tailwind CSS',
8};
9
10export default function RootLayout({
11 children,
12}: {
13 children: React.ReactNode;
14}) {
15 return (
16 <html lang="en">
17 <body className="bg-gray-50 text-gray-900 antialiased">
18 <nav className="bg-white shadow-md">
19 <div className="container mx-auto px-4 py-4">
20 <h1 className="text-2xl font-bold">My App</h1>
21 </div>
22 </nav>
23 <main className="container mx-auto px-4 py-8">
24 {children}
25 </main>
26 </body>
27 </html>
28 );
29}Page Component:
1// app/page.tsx
2export default function Home() {
3 return (
4 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
5 <div className="bg-white p-6 rounded-lg shadow-md">
6 <h2 className="text-xl font-bold mb-2">Card 1</h2>
7 <p className="text-gray-600">Content goes here</p>
8 </div>
9 {/* More cards */}
10 </div>
11 );
12}1// pages/_app.tsx
2import '../styles/globals.css';
3import type { AppProps } from 'next/app';
4
5export default function App({ Component, pageProps }: AppProps) {
6 return (
7 <div className="min-h-screen bg-gray-50">
8 <Component {...pageProps} />
9 </div>
10 );
11}1// app/products/page.tsx
2async function getProducts() {
3 const res = await fetch('https://api.example.com/products');
4 return res.json();
5}
6
7export default async function ProductsPage() {
8 const products = await getProducts();
9
10 return (
11 <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
12 {products.map((product) => (
13 <div key={product.id} className="bg-white rounded-lg shadow-md p-6">
14 <h3 className="text-xl font-bold mb-2">{product.name}</h3>
15 <p className="text-gray-600 mb-4">{product.description}</p>
16 <button className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
17 View Details
18 </button>
19 </div>
20 ))}
21 </div>
22 );
23}1// components/InteractiveCard.tsx
2'use client';
3
4import { useState } from 'react';
5
6export function InteractiveCard({ title, content }: { title: string; content: string }) {
7 const [isExpanded, setIsExpanded] = useState(false);
8
9 return (
10 <div className="bg-white rounded-lg shadow-md p-6">
11 <h3 className="text-xl font-bold mb-2">{title}</h3>
12 <p className={isExpanded ? 'block' : 'line-clamp-3'}>
13 {content}
14 </p>
15 <button
16 onClick={() => setIsExpanded(!isExpanded)}
17 className="mt-2 text-blue-600 hover:text-blue-800 font-semibold"
18 >
19 {isExpanded ? 'Show Less' : 'Read More'}
20 </button>
21 </div>
22 );
23}Next.js automatically optimizes Tailwind CSS:
Additional Optimization:
1// next.config.js
2/** @type {import('next').NextConfig} */
3const nextConfig = {
4 experimental: {
5 optimizeCss: true, // Enable CSS optimization
6 },
7};
8
9module.exports = nextConfig;Vue.js and Nuxt have excellent Tailwind CSS support with reactive class binding.
1# Create new Vue app
2npm create vue@latest my-app
3cd my-app
4
5# Install Tailwind CSS
6npm install -D tailwindcss postcss autoprefixer
7npx tailwindcss init -p1// tailwind.config.js
2/** @type {import('tailwindcss').Config} */
3module.exports = {
4 content: [
5 "./index.html",
6 "./src/**/*.{vue,js,ts,jsx,tsx}",
7 ],
8 theme: {
9 extend: {},
10 },
11 plugins: [],
12}1/* src/assets/main.css */
2@tailwind base;
3@tailwind components;
4@tailwind utilities;1// src/main.js
2import { createApp } from 'vue';
3import App from './App.vue';
4import './assets/main.css'; // Import Tailwind
5
6createApp(App).mount('#app');Single File Component:
1<!-- components/Button.vue -->
2<template>
3 <button
4 :class="buttonClasses"
5 @click="$emit('click', $event)"
6 >
7 <slot />
8 </button>
9</template>
10
11<script setup>
12import { computed } from 'vue';
13
14const props = defineProps({
15 variant: {
16 type: String,
17 default: 'primary',
18 validator: (value) => ['primary', 'secondary', 'outline'].includes(value)
19 }
20});
21
22const buttonClasses = computed(() => {
23 const base = 'px-4 py-2 rounded-lg font-semibold transition-colors';
24
25 const variants = {
26 primary: 'bg-blue-500 hover:bg-blue-600 text-white',
27 secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
28 outline: 'border-2 border-blue-500 text-blue-500 hover:bg-blue-50',
29 };
30
31 return `${base} ${variants[props.variant]}`;
32});
33</script>Usage:
1<template>
2 <div class="container mx-auto p-4">
3 <Button variant="primary" @click="handleClick">
4 Primary Button
5 </Button>
6 <Button variant="secondary">
7 Secondary Button
8 </Button>
9 </div>
10</template>
11
12<script setup>
13import Button from './components/Button.vue';
14
15const handleClick = () => {
16 console.log('Button clicked');
17};
18</script>Class Binding:
1<template>
2 <!-- Object syntax -->
3 <div :class="{
4 'bg-green-100': isSuccess,
5 'bg-red-100': isError,
6 'bg-blue-100': !isSuccess && !isError
7 }">
8 Status message
9 </div>
10
11 <!-- Array syntax -->
12 <div :class="[
13 'p-4 rounded-lg',
14 isActive ? 'bg-blue-500 text-white' : 'bg-gray-200'
15 ]">
16 Dynamic content
17 </div>
18
19 <!-- Computed property -->
20 <div :class="cardClasses">
21 Card with computed classes
22 </div>
23</template>
24
25<script setup>
26import { ref, computed } from 'vue';
27
28const isSuccess = ref(false);
29const isError = ref(false);
30const isActive = ref(true);
31
32const cardClasses = computed(() => ({
33 'bg-white': true,
34 'rounded-lg': true,
35 'shadow-md': true,
36 'p-6': true,
37 'border-2': isActive.value,
38 'border-blue-500': isActive.value,
39}));
40</script>1# Create new Nuxt app
2npx nuxi@latest init my-app
3cd my-app
4
5# Install Tailwind module
6npm install -D @nuxtjs/tailwindcssConfiguration:
1// nuxt.config.ts
2export default defineNuxtConfig({
3 modules: ['@nuxtjs/tailwindcss'],
4 tailwindcss: {
5 cssPath: '~/assets/css/tailwind.css',
6 configPath: 'tailwind.config',
7 exposeConfig: false,
8 config: {},
9 }
10});1// tailwind.config.js
2/** @type {import('tailwindcss').Config} */
3module.exports = {
4 content: [
5 "./components/**/*.{js,vue,ts}",
6 "./layouts/**/*.vue",
7 "./pages/**/*.vue",
8 "./plugins/**/*.{js,ts}",
9 "./app.vue",
10 ],
11 theme: {
12 extend: {},
13 },
14 plugins: [],
15}Nuxt Page:
1<!-- pages/index.vue -->
2<template>
3 <div class="container mx-auto px-4 py-8">
4 <h1 class="text-4xl font-bold text-gray-900 mb-6">
5 Welcome to Nuxt
6 </h1>
7
8 <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
9 <div v-for="item in items" :key="item.id" class="bg-white rounded-lg shadow-md p-6">
10 <h2 class="text-xl font-bold mb-2">{{ item.title }}</h2>
11 <p class="text-gray-600">{{ item.description }}</p>
12 </div>
13 </div>
14 </div>
15</template>
16
17<script setup>
18const items = ref([
19 { id: 1, title: 'Item 1', description: 'Description 1' },
20 { id: 2, title: 'Item 2', description: 'Description 2' },
21 { id: 3, title: 'Item 3', description: 'Description 3' },
22]);
23</script>1<!-- components/Card.vue -->
2<template>
3 <div class="bg-white rounded-lg shadow-md p-6">
4 <h3 class="text-xl font-bold mb-2">{{ title }}</h3>
5 <p class="text-gray-600">{{ description }}</p>
6 <slot name="footer" />
7 </div>
8</template>
9
10<script setup lang="ts">
11interface Props {
12 title: string;
13 description: string;
14}
15
16defineProps<Props>();
17</script>Svelte has a streamlined approach to Tailwind CSS integration with minimal configuration.
1# Create new SvelteKit app
2npm create svelte@latest my-app
3cd my-app
4
5# Install Tailwind CSS
6npm install -D tailwindcss postcss autoprefixer
7npx tailwindcss init -p1// tailwind.config.js
2/** @type {import('tailwindcss').Config} */
3module.exports = {
4 content: ['./src/**/*.{html,js,svelte,ts}'],
5 theme: {
6 extend: {}
7 },
8 plugins: []
9};1/* src/app.css */
2@tailwind base;
3@tailwind components;
4@tailwind utilities;1<!-- src/routes/+layout.svelte -->
2<script>
3 import '../app.css';
4</script>
5
6<div class="min-h-screen bg-gray-50">
7 <nav class="bg-white shadow-md">
8 <div class="container mx-auto px-4 py-4">
9 <h1 class="text-2xl font-bold">My SvelteKit App</h1>
10 </div>
11 </nav>
12
13 <main class="container mx-auto px-4 py-8">
14 <slot />
15 </main>
16</div>Button Component:
1<!-- src/lib/components/Button.svelte -->
2<script>
3 export let variant = 'primary';
4 export let disabled = false;
5
6 const baseClasses = 'px-4 py-2 rounded-lg font-semibold transition-colors';
7
8 const variantClasses = {
9 primary: 'bg-blue-500 hover:bg-blue-600 text-white',
10 secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
11 outline: 'border-2 border-blue-500 text-blue-500 hover:bg-blue-50',
12 };
13
14 $: buttonClasses = `${baseClasses} ${variantClasses[variant]} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`;
15</script>
16
17<button class={buttonClasses} {disabled} on:click>
18 <slot />
19</button>Usage:
1<script>
2 import Button from '$lib/components/Button.svelte';
3
4 function handleClick() {
5 console.log('Button clicked');
6 }
7</script>
8
9<div class="space-x-4">
10 <Button variant="primary" on:click={handleClick}>
11 Primary
12 </Button>
13 <Button variant="secondary">
14 Secondary
15 </Button>
16 <Button variant="outline" disabled>
17 Disabled
18 </Button>
19</div>Reactive Classes:
1<script>
2 let isActive = false;
3 let count = 0;
4
5 $: cardClasses = [
6 'p-4 rounded-lg',
7 isActive ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-900',
8 count > 5 ? 'border-4 border-red-500' : ''
9 ].join(' ');
10</script>
11
12<div class={cardClasses}>
13 Count: {count}
14 <button
15 class="ml-4 px-3 py-1 bg-white text-gray-900 rounded"
16 on:click={() => count++}
17 >
18 Increment
19 </button>
20</div>Class Directive:
1<script>
2 let isSuccess = false;
3 let isError = false;
4</script>
5
6<div
7 class="p-4 rounded-lg"
8 class:bg-green-100={isSuccess}
9 class:bg-red-100={isError}
10 class:bg-blue-100={!isSuccess && !isError}
11>
12 Status message
13</div>1<!-- src/lib/components/Card.svelte -->
2<script lang="ts">
3 export let title: string;
4 export let description: string;
5 export let image: string | undefined = undefined;
6</script>
7
8<div class="bg-white rounded-lg shadow-md overflow-hidden">
9 {#if image}
10 <img src={image} alt={title} class="w-full h-48 object-cover" />
11 {/if}
12 <div class="p-6">
13 <h3 class="text-xl font-bold text-gray-900 mb-2">{title}</h3>
14 <p class="text-gray-600">{description}</p>
15 </div>
16 <div class="bg-gray-50 px-6 py-4 border-t border-gray-200">
17 <slot name="footer" />
18 </div>
19</div>1<!-- src/routes/+page.svelte -->
2<script lang="ts">
3 import Card from '$lib/components/Card.svelte';
4 import Button from '$lib/components/Button.svelte';
5
6 const products = [
7 { id: 1, title: 'Product 1', description: 'Description 1', image: '/images/product1.jpg' },
8 { id: 2, title: 'Product 2', description: 'Description 2', image: '/images/product2.jpg' },
9 { id: 3, title: 'Product 3', description: 'Description 3', image: '/images/product3.jpg' },
10 ];
11</script>
12
13<h1 class="text-4xl font-bold text-gray-900 mb-8">
14 Our Products
15</h1>
16
17<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
18 {#each products as product (product.id)}
19 <Card
20 title={product.title}
21 description={product.description}
22 image={product.image}
23 >
24 <svelte:fragment slot="footer">
25 <Button variant="primary">Buy Now</Button>
26 </svelte:fragment>
27 </Card>
28 {/each}
29</div>Universal best practices that apply to all framework integrations.
Folder Structure:
1src/
2├── components/
3│ ├── ui/ # Base UI components
4│ │ ├── Button.tsx
5│ │ ├── Card.tsx
6│ │ ├── Input.tsx
7│ │ └── Modal.tsx
8│ ├── layout/ # Layout components
9│ │ ├── Header.tsx
10│ │ ├── Footer.tsx
11│ │ └── Sidebar.tsx
12│ └── features/ # Feature-specific components
13│ ├── ProductCard.tsx
14│ └── UserProfile.tsx
15├── styles/
16│ └── globals.css # Tailwind imports
17└── utils/
18 └── classNames.ts # Class utility helpersclassNames Helper:
1// utils/classNames.ts
2export function cn(...classes: (string | undefined | null | false)[]): string {
3 return classes.filter(Boolean).join(' ');
4}
5
6// Usage
7import { cn } from '@/utils/classNames';
8
9<div className={cn(
10 'p-4 rounded-lg',
11 isActive && 'bg-blue-500',
12 isDisabled && 'opacity-50'
13)}>
14 Content
15</div>Variant Function:
1// utils/variants.ts
2export function cva(base: string, config: Record<string, Record<string, string>>) {
3 return (variants: Record<string, string>) => {
4 const classes = [base];
5
6 for (const [key, value] of Object.entries(variants)) {
7 if (config[key]?.[value]) {
8 classes.push(config[key][value]);
9 }
10 }
11
12 return classes.join(' ');
13 };
14}
15
16// Usage
17const buttonVariants = cva('px-4 py-2 rounded-lg font-semibold', {
18 variant: {
19 primary: 'bg-blue-500 text-white hover:bg-blue-600',
20 secondary: 'bg-gray-500 text-white hover:bg-gray-600',
21 },
22 size: {
23 sm: 'text-sm py-1 px-3',
24 md: 'text-base py-2 px-4',
25 lg: 'text-lg py-3 px-6',
26 },
27});
28
29<button className={buttonVariants({ variant: 'primary', size: 'lg' })}>
30 Large Primary Button
31</button>1. Avoid Dynamic Class Construction:
1// ❌ Bad - dynamic construction not detected
2const size = 'large';
3<div className={`text-${size}`}>
4
5// ✅ Good - complete class names
6<div className={size === 'large' ? 'text-lg' : 'text-base'}>2. Extract Repeated Patterns:
1// ❌ Bad - repeated classes everywhere
2<div className="bg-white rounded-lg shadow-md p-6">...</div>
3<div className="bg-white rounded-lg shadow-md p-6">...</div>
4<div className="bg-white rounded-lg shadow-md p-6">...</div>
5
6// ✅ Good - reusable component
7function Card({ children }) {
8 return (
9 <div className="bg-white rounded-lg shadow-md p-6">
10 {children}
11 </div>
12 );
13}3. Use CSS Modules for Complex Components:
1// components/ComplexComponent.module.css
2@layer components {
3 .complex-component {
4 @apply bg-white rounded-lg shadow-md p-6;
5 @apply hover:shadow-xl transition-shadow duration-300;
6
7 /* Complex custom styles */
8 background-image: linear-gradient(to right, theme('colors.blue.500'), theme('colors.purple.500'));
9 }
10}
11
12// components/ComplexComponent.tsx
13import styles from './ComplexComponent.module.css';
14
15export function ComplexComponent() {
16 return <div className={styles.complexComponent}>...</div>;
17}ARIA Attributes:
1function Button({ children, loading, ...props }) {
2 return (
3 <button
4 className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
5 aria-busy={loading}
6 aria-disabled={loading}
7 {...props}
8 >
9 {loading ? 'Loading...' : children}
10 </button>
11 );
12}Focus States:
1<button className="
2 px-4 py-2 bg-blue-500 text-white rounded
3 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
4 hover:bg-blue-600
5 disabled:opacity-50 disabled:cursor-not-allowed
6">
7 Accessible Button
8</button>Using class-variance-authority (CVA):
1npm install class-variance-authority1import { cva, type VariantProps } from 'class-variance-authority';
2
3const buttonVariants = cva(
4 'px-4 py-2 rounded-lg font-semibold transition-colors',
5 {
6 variants: {
7 variant: {
8 primary: 'bg-blue-500 hover:bg-blue-600 text-white',
9 secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
10 outline: 'border-2 border-blue-500 text-blue-500 hover:bg-blue-50',
11 },
12 size: {
13 sm: 'text-sm py-1 px-3',
14 md: 'text-base py-2 px-4',
15 lg: 'text-lg py-3 px-6',
16 },
17 },
18 defaultVariants: {
19 variant: 'primary',
20 size: 'md',
21 },
22 }
23);
24
25interface ButtonProps
26 extends React.ButtonHTMLAttributes<HTMLButtonElement>,
27 VariantProps<typeof buttonVariants> {}
28
29export function Button({ variant, size, className, ...props }: ButtonProps) {
30 return (
31 <button
32 className={buttonVariants({ variant, size, className })}
33 {...props}
34 />
35 );
36}Component Testing with Tailwind:
1// Button.test.tsx
2import { render, screen } from '@testing-library/react';
3import { Button } from './Button';
4
5describe('Button', () => {
6 it('renders with primary variant classes', () => {
7 render(<Button variant="primary">Click me</Button>);
8 const button = screen.getByRole('button');
9
10 expect(button).toHaveClass('bg-blue-500');
11 expect(button).toHaveClass('text-white');
12 });
13
14 it('renders with secondary variant classes', () => {
15 render(<Button variant="secondary">Click me</Button>);
16 const button = screen.getByRole('button');
17
18 expect(button).toHaveClass('bg-gray-500');
19 });
20});Build a complete, reusable component library that works across React, Vue, and Svelte with Tailwind CSS.
1// ===== REACT VERSION =====
2// components/Button.tsx (React)
3import { cva, type VariantProps } from 'class-variance-authority';
4import type { ButtonHTMLAttributes } from 'react';
5
6const buttonVariants = cva(
7 'inline-flex items-center justify-center rounded-lg font-semibold transition-all',
8 {
9 variants: {
10 variant: {
11 primary: 'bg-blue-500 text-white hover:bg-blue-600 focus:ring-blue-500',
12 secondary: 'bg-gray-500 text-white hover:bg-gray-600 focus:ring-gray-500',
13 outline: 'border-2 border-blue-500 text-blue-500 hover:bg-blue-50 focus:ring-blue-500',
14 ghost: 'text-gray-700 hover:bg-gray-100 focus:ring-gray-500',
15 danger: 'bg-red-500 text-white hover:bg-red-600 focus:ring-red-500',
16 },
17 size: {
18 sm: 'text-sm px-3 py-1.5',
19 md: 'text-base px-4 py-2',
20 lg: 'text-lg px-6 py-3',
21 },
22 fullWidth: {
23 true: 'w-full',
24 },
25 },
26 defaultVariants: {
27 variant: 'primary',
28 size: 'md',
29 },
30 }
31);
32
33interface ButtonProps
34 extends ButtonHTMLAttributes<HTMLButtonElement>,
35 VariantProps<typeof buttonVariants> {
36 loading?: boolean;
37}
38
39export function Button({
40 variant,
41 size,
42 fullWidth,
43 loading,
44 disabled,
45 children,
46 className,
47 ...props
48}: ButtonProps) {
49 return (
50 <button
51 className={`${buttonVariants({ variant, size, fullWidth })} ${
52 loading || disabled ? 'opacity-50 cursor-not-allowed' : ''
53 } ${className || ''}`}
54 disabled={loading || disabled}
55 {...props}
56 >
57 {loading && (
58 <svg
59 className="animate-spin -ml-1 mr-2 h-4 w-4"
60 fill="none"
61 viewBox="0 0 24 24"
62 >
63 <circle
64 className="opacity-25"
65 cx="12"
66 cy="12"
67 r="10"
68 stroke="currentColor"
69 strokeWidth="4"
70 />
71 <path
72 className="opacity-75"
73 fill="currentColor"
74 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
75 />
76 </svg>
77 )}
78 {children}
79 </button>
80 );
81}
82
83// components/Card.tsx (React)
84import type { ReactNode } from 'react';
85
86interface CardProps {
87 title: string;
88 description: string;
89 image?: string;
90 badge?: string;
91 footer?: ReactNode;
92 className?: string;
93}
94
95export function Card({
96 title,
97 description,
98 image,
99 badge,
100 footer,
101 className,
102}: CardProps) {
103 return (
104 <div
105 className={`bg-white rounded-xl shadow-md overflow-hidden transition-shadow duration-300 hover:shadow-xl ${
106 className || ''
107 }`}
108 >
109 {image && (
110 <div className="relative h-48 overflow-hidden">
111 <img
112 src={image}
113 alt={title}
114 className="w-full h-full object-cover transition-transform duration-300 hover:scale-105"
115 />
116 {badge && (
117 <span className="absolute top-4 right-4 px-3 py-1 bg-white/90 backdrop-blur-sm rounded-full text-sm font-semibold text-gray-900">
118 {badge}
119 </span>
120 )}
121 </div>
122 )}
123
124 <div className="p-6">
125 <h3 className="text-xl font-bold text-gray-900 mb-2">{title}</h3>
126 <p className="text-gray-600 line-clamp-3">{description}</p>
127 </div>
128
129 {footer && (
130 <div className="bg-gray-50 px-6 py-4 border-t border-gray-200">
131 {footer}
132 </div>
133 )}
134 </div>
135 );
136}
137
138// ===== VUE VERSION =====
139<!-- Button.vue (Vue 3) -->
140<template>
141 <button
142 :class="buttonClasses"
143 :disabled="loading || disabled"
144 @click="$emit('click', $event)"
145 >
146 <svg
147 v-if="loading"
148 class="animate-spin -ml-1 mr-2 h-4 w-4"
149 fill="none"
150 viewBox="0 0 24 24"
151 >
152 <circle
153 class="opacity-25"
154 cx="12"
155 cy="12"
156 r="10"
157 stroke="currentColor"
158 stroke-width="4"
159 />
160 <path
161 class="opacity-75"
162 fill="currentColor"
163 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
164 />
165 </svg>
166 <slot />
167 </button>
168</template>
169
170<script setup lang="ts">
171import { computed } from 'vue';
172
173const props = defineProps({
174 variant: {
175 type: String,
176 default: 'primary',
177 validator: (v: string) => ['primary', 'secondary', 'outline', 'ghost', 'danger'].includes(v),
178 },
179 size: {
180 type: String,
181 default: 'md',
182 validator: (v: string) => ['sm', 'md', 'lg'].includes(v),
183 },
184 fullWidth: Boolean,
185 loading: Boolean,
186 disabled: Boolean,
187});
188
189defineEmits(['click']);
190
191const buttonClasses = computed(() => {
192 const base = 'inline-flex items-center justify-center rounded-lg font-semibold transition-all';
193
194 const variants = {
195 primary: 'bg-blue-500 text-white hover:bg-blue-600 focus:ring-blue-500',
196 secondary: 'bg-gray-500 text-white hover:bg-gray-600 focus:ring-gray-500',
197 outline: 'border-2 border-blue-500 text-blue-500 hover:bg-blue-50 focus:ring-blue-500',
198 ghost: 'text-gray-700 hover:bg-gray-100 focus:ring-gray-500',
199 danger: 'bg-red-500 text-white hover:bg-red-600 focus:ring-red-500',
200 };
201
202 const sizes = {
203 sm: 'text-sm px-3 py-1.5',
204 md: 'text-base px-4 py-2',
205 lg: 'text-lg px-6 py-3',
206 };
207
208 return [
209 base,
210 variants[props.variant],
211 sizes[props.size],
212 props.fullWidth && 'w-full',
213 (props.loading || props.disabled) && 'opacity-50 cursor-not-allowed',
214 ].filter(Boolean).join(' ');
215});
216</script>
217
218// ===== SVELTE VERSION =====
219<!-- Button.svelte (Svelte) -->
220<script lang="ts">
221 export let variant: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' = 'primary';
222 export let size: 'sm' | 'md' | 'lg' = 'md';
223 export let fullWidth = false;
224 export let loading = false;
225 export let disabled = false;
226
227 const baseClasses = 'inline-flex items-center justify-center rounded-lg font-semibold transition-all';
228
229 const variants = {
230 primary: 'bg-blue-500 text-white hover:bg-blue-600 focus:ring-blue-500',
231 secondary: 'bg-gray-500 text-white hover:bg-gray-600 focus:ring-gray-500',
232 outline: 'border-2 border-blue-500 text-blue-500 hover:bg-blue-50 focus:ring-blue-500',
233 ghost: 'text-gray-700 hover:bg-gray-100 focus:ring-gray-500',
234 danger: 'bg-red-500 text-white hover:bg-red-600 focus:ring-red-500',
235 };
236
237 const sizes = {
238 sm: 'text-sm px-3 py-1.5',
239 md: 'text-base px-4 py-2',
240 lg: 'text-lg px-6 py-3',
241 };
242
243 $: buttonClasses = [
244 baseClasses,
245 variants[variant],
246 sizes[size],
247 fullWidth && 'w-full',
248 (loading || disabled) && 'opacity-50 cursor-not-allowed',
249 ].filter(Boolean).join(' ');
250</script>
251
252<button
253 class={buttonClasses}
254 disabled={loading || disabled}
255 on:click
256>
257 {#if loading}
258 <svg
259 class="animate-spin -ml-1 mr-2 h-4 w-4"
260 fill="none"
261 viewBox="0 0 24 24"
262 >
263 <circle
264 class="opacity-25"
265 cx="12"
266 cy="12"
267 r="10"
268 stroke="currentColor"
269 stroke-width="4"
270 />
271 <path
272 class="opacity-75"
273 fill="currentColor"
274 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
275 />
276 </svg>
277 {/if}
278 <slot />
279</button>This comprehensive example demonstrates a universal component library approach:
1. React Implementation:
2. Vue Implementation:
3. Svelte Implementation:
Common Patterns Across All Frameworks:
Usage Examples:
1// React
2<Button variant="primary" size="lg" loading>
3 Save Changes
4</Button>
5
6// Vue
7<Button variant="primary" size="lg" :loading="true">
8 Save Changes
9</Button>
10
11// Svelte
12<Button variant="primary" size="lg" loading>
13 Save Changes
14</Button>Key Benefits:
This pattern can be extended to build complete design systems with buttons, cards, inputs, modals, and more, all sharing the same visual language while leveraging each framework's strengths.
Now that you've mastered framework integration, you're ready to:
Challenge: Build a complete e-commerce product page with 10+ reusable components (Button, Card, Input, Modal, etc.) using your framework of choice and Tailwind CSS!
Your progress is saved in this browser, on this device only.