Tailwind CSS

Tailwind with React & Vue

20 min Lesson 26 of 35

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;
Important: In React, you must use 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>
  );
}
Pro Tip: Use the 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>
Why tailwind-merge? Without it, if you pass 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>
Vue Advantage: Vue's class binding is native and doesn't require external libraries like 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>
  );
}
Avoid Dynamic Classes: Never generate Tailwind classes dynamically with string interpolation like 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.)