Skip to content

🎨 TailwindCSS

📋 Vue d'ensemble

TailwindCSS est un framework CSS utility-first qui permet de construire des interfaces rapidement en utilisant des classes utilitaires pré-définies. Il offre un système de design cohérent et responsive.

🏗️ Concepts fondamentaux

Approche utility-first

TailwindCSS suit le principe utility-first : au lieu d'écrire du CSS personnalisé, vous composez des interfaces en utilisant des classes utilitaires.

tsx
// ❌ CSS traditionnel
<div className="event-card">
  <h3 className="event-title">Match d'improvisation</h3>
  <p className="event-description">Un spectacle passionnant</p>
</div>

// ✅ TailwindCSS utility-first
<div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
  <h3 className="text-xl font-bold text-gray-900 mb-2">Match d'improvisation</h3>
  <p className="text-gray-600">Un spectacle passionnant</p>
</div>

Classes utilitaires

Les classes TailwindCSS sont organisées par catégories :

tsx
// Layout et espacement
<div className="container mx-auto px-4 py-8">
  <div className="max-w-4xl mx-auto">
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
      {/* Contenu */}
    </div>
  </div>
</div>

// Typographie
<h1 className="text-4xl font-bold text-gray-900 mb-4">
  Nos spectacles
</h1>
<p className="text-lg text-gray-600 leading-relaxed">
  Découvrez tous nos événements d'improvisation
</p>

// Couleurs et arrière-plans
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
  <span className="text-blue-800 font-medium">Information</span>
</div>

// États et interactions
<button className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
  Participer
</button>

🎨 Système de couleurs

Palette de couleurs par défaut

TailwindCSS fournit une palette de couleurs complète avec des variantes :

tsx
// Couleurs principales
<div className="space-y-4">
  {/* Bleu */}
  <div className="flex space-x-2">
    <div className="w-16 h-16 bg-blue-50 rounded"></div>
    <div className="w-16 h-16 bg-blue-100 rounded"></div>
    <div className="w-16 h-16 bg-blue-200 rounded"></div>
    <div className="w-16 h-16 bg-blue-300 rounded"></div>
    <div className="w-16 h-16 bg-blue-400 rounded"></div>
    <div className="w-16 h-16 bg-blue-500 rounded"></div>
    <div className="w-16 h-16 bg-blue-600 rounded"></div>
    <div className="w-16 h-16 bg-blue-700 rounded"></div>
    <div className="w-16 h-16 bg-blue-800 rounded"></div>
    <div className="w-16 h-16 bg-blue-900 rounded"></div>
  </div>

  {/* Vert */}
  <div className="flex space-x-2">
    <div className="w-16 h-16 bg-green-50 rounded"></div>
    <div className="w-16 h-16 bg-green-100 rounded"></div>
    <div className="w-16 h-16 bg-green-200 rounded"></div>
    <div className="w-16 h-16 bg-green-300 rounded"></div>
    <div className="w-16 h-16 bg-green-400 rounded"></div>
    <div className="w-16 h-16 bg-green-500 rounded"></div>
    <div className="w-16 h-16 bg-green-600 rounded"></div>
    <div className="w-16 h-16 bg-green-700 rounded"></div>
    <div className="w-16 h-16 bg-green-800 rounded"></div>
    <div className="w-16 h-16 bg-green-900 rounded"></div>
  </div>

  {/* Rouge */}
  <div className="flex space-x-2">
    <div className="w-16 h-16 bg-red-50 rounded"></div>
    <div className="w-16 h-16 bg-red-100 rounded"></div>
    <div className="w-16 h-16 bg-red-200 rounded"></div>
    <div className="w-16 h-16 bg-red-300 rounded"></div>
    <div className="w-16 h-16 bg-red-400 rounded"></div>
    <div className="w-16 h-16 bg-red-500 rounded"></div>
    <div className="w-16 h-16 bg-red-600 rounded"></div>
    <div className="w-16 h-16 bg-red-700 rounded"></div>
    <div className="w-16 h-16 bg-red-800 rounded"></div>
    <div className="w-16 h-16 bg-red-900 rounded"></div>
  </div>
</div>

Utilisation des couleurs

tsx
// Arrière-plans
<div className="bg-white">Arrière-plan blanc</div>
<div className="bg-gray-50">Arrière-plan gris très clair</div>
<div className="bg-blue-500">Arrière-plan bleu</div>

// Texte
<p className="text-black">Texte noir</p>
<p className="text-gray-600">Texte gris</p>
<p className="text-blue-600">Texte bleu</p>

// Bordures
<div className="border border-gray-200">Bordure grise</div>
<div className="border-2 border-blue-500">Bordure bleue épaisse</div>

// États
<button className="bg-blue-600 hover:bg-blue-700 active:bg-blue-800">
  Bouton avec états
</button>

📱 Système responsive

Breakpoints par défaut

TailwindCSS utilise des breakpoints mobiles-first :

tsx
// Mobile-first approach
<div className="
  w-full          /* Mobile: 100% */
  md:w-1/2       /* Medium (768px+): 50% */
  lg:w-1/3       /* Large (1024px+): 33.33% */
  xl:w-1/4       /* Extra Large (1280px+): 25% */
">
  Contenu responsive
</div>

// Grille responsive
<div className="
  grid
  grid-cols-1    /* Mobile: 1 colonne */
  md:grid-cols-2 /* Medium: 2 colonnes */
  lg:grid-cols-3 /* Large: 3 colonnes */
  xl:grid-cols-4 /* Extra Large: 4 colonnes */
  gap-4
">
  {/* Éléments de la grille */}
</div>

// Espacement responsive
<div className="
  p-4            /* Mobile: padding 16px */
  md:p-6         /* Medium: padding 24px */
  lg:p-8         /* Large: padding 32px */
">
  Contenu avec espacement adaptatif
</div>

Classes conditionnelles

tsx
// Affichage conditionnel
<div className="
  hidden          /* Caché par défaut */
  md:block       /* Visible sur medium+ */
">
  Visible seulement sur desktop
</div>

<div className="
  block           /* Visible par défaut */
  lg:hidden       /* Caché sur large+ */
">
  Visible seulement sur mobile/tablet
</div>

// Typographie responsive
<h1 className="
  text-2xl        /* Mobile: 24px */
  md:text-3xl     /* Medium: 30px */
  lg:text-4xl     /* Large: 36px */
  xl:text-5xl     /* Extra Large: 48px */
  font-bold
">
  Titre responsive
</h1>

🎯 Composants réutilisables

Carte d'événement

tsx
function EventCard({ event }: { event: Event }) {
  return (
    <div className="
      bg-white 
      rounded-lg 
      shadow-md 
      overflow-hidden 
      hover:shadow-lg 
      transition-shadow 
      duration-300
      cursor-pointer
    ">
      {/* Image de l'événement */}
      <div className="
        aspect-video 
        bg-gradient-to-br 
        from-blue-400 
        to-purple-600 
        flex 
        items-center 
        justify-center
      ">
        <span className="text-white text-4xl">🎭</span>
      </div>
      
      {/* Contenu */}
      <div className="p-6">
        <h3 className="
          text-xl 
          font-semibold 
          text-gray-900 
          mb-2 
          line-clamp-2
        ">
          {event.title}
        </h3>
        
        <p className="
          text-gray-600 
          text-sm 
          mb-4 
          line-clamp-3
        ">
          {event.description}
        </p>
        
        {/* Métadonnées */}
        <div className="
          flex 
          items-center 
          justify-between 
          text-sm 
          text-gray-500 
          mb-4
        ">
          <span className="flex items-center gap-1">
            <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
            </svg>
            {new Date(event.date).toLocaleDateString('fr-FR')}
          </span>
          
          <span className="flex items-center gap-1">
            <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
            </svg>
            {event.maxPlayers} joueurs
          </span>
        </div>
        
        {/* Actions */}
        <div className="flex gap-2">
          <button className="
            flex-1 
            px-4 
            py-2 
            bg-blue-600 
            text-white 
            text-sm 
            font-medium 
            rounded-md 
            hover:bg-blue-700 
            focus:outline-none 
            focus:ring-2 
            focus:ring-blue-500 
            focus:ring-offset-2 
            transition-colors
          ">
            Voir détails
          </button>
          
          <button className="
            px-4 
            py-2 
            border 
            border-gray-300 
            text-gray-700 
            text-sm 
            font-medium 
            rounded-md 
            hover:bg-gray-50 
            focus:outline-none 
            focus:ring-2 
            focus:ring-blue-500 
            focus:ring-offset-2 
            transition-colors
          ">
            <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
            </svg>
          </button>
        </div>
      </div>
    </div>
  );
}

Formulaire stylisé

tsx
function EventForm() {
  return (
    <form className="space-y-6">
      <div>
        <label htmlFor="title" className="
          block 
          text-sm 
          font-medium 
          text-gray-700 
          mb-2
        ">
          Titre de l'événement *
        </label>
        <input
          type="text"
          id="title"
          className="
            w-full 
            px-3 
            py-2 
            border 
            border-gray-300 
            rounded-md 
            focus:outline-none 
            focus:ring-2 
            focus:ring-blue-500 
            focus:border-transparent
            placeholder-gray-400
            transition-colors
          "
          placeholder="Ex: Match d'improvisation"
        />
      </div>

      <div>
        <label htmlFor="description" className="
          block 
          text-sm 
          font-medium 
          text-gray-700 
          mb-2
        ">
          Description *
        </label>
        <textarea
          id="description"
          rows={4}
          className="
            w-full 
            px-3 
            py-2 
            border 
            border-gray-300 
            rounded-md 
            focus:outline-none 
            focus:ring-2 
            focus:ring-blue-500 
            focus:border-transparent
            placeholder-gray-400
            transition-colors
            resize-vertical
          "
          placeholder="Décrivez votre événement..."
        />
      </div>

      <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
        <div>
          <label htmlFor="date" className="
            block 
            text-sm 
            font-medium 
            text-gray-700 
            mb-2
          ">
            Date et heure *
          </label>
          <input
            type="datetime-local"
            id="date"
            className="
              w-full 
              px-3 
              py-2 
              border 
              border-gray-300 
              rounded-md 
              focus:outline-none 
              focus:ring-2 
              focus:ring-blue-500 
              focus:border-transparent
              transition-colors
            "
          />
        </div>

        <div>
          <label htmlFor="maxPlayers" className="
            block 
            text-sm 
            font-medium 
            text-gray-700 
            mb-2
          ">
            Nombre max de joueurs *
          </label>
          <input
            type="number"
            id="maxPlayers"
            min="1"
            max="50"
            className="
              w-full 
              px-3 
              py-2 
              border 
              border-gray-300 
              rounded-md 
              focus:outline-none 
              focus:ring-2 
              focus:ring-blue-500 
              focus:border-transparent
              transition-colors
            "
          />
        </div>
      </div>

      <div className="flex justify-end gap-4">
        <button
          type="button"
          className="
            px-4 
            py-2 
            text-gray-700 
            bg-gray-100 
            rounded-md 
            hover:bg-gray-200 
            focus:outline-none 
            focus:ring-2 
            focus:ring-gray-500 
            focus:ring-offset-2 
            transition-colors
          "
        >
          Réinitialiser
        </button>
        
        <button
          type="submit"
          className="
            px-6 
            py-2 
            bg-blue-600 
            text-white 
            rounded-md 
            hover:bg-blue-700 
            focus:outline-none 
            focus:ring-2 
            focus:ring-blue-500 
            focus:ring-offset-2 
            transition-colors
          "
        >
          Créer l'événement
        </button>
      </div>
    </form>
  );
}

🎨 Personnalisation du design system

Configuration TailwindCSS

javascript
// tailwind.config.js
module.exports = {
  content: [
    './app/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {
      colors: {
        // Couleurs personnalisées LIPAIX
        primary: {
          50: '#eff6ff',
          100: '#dbeafe',
          200: '#bfdbfe',
          300: '#93c5fd',
          400: '#60a5fa',
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8',
          800: '#1e40af',
          900: '#1e3a8a',
        },
        secondary: {
          50: '#fdf4ff',
          100: '#fae8ff',
          200: '#f5d0fe',
          300: '#f0abfc',
          400: '#e879f9',
          500: '#d946ef',
          600: '#c026d3',
          700: '#a21caf',
          800: '#86198f',
          900: '#701a75',
        },
        accent: {
          50: '#fefce8',
          100: '#fef9c3',
          200: '#fef08a',
          300: '#fde047',
          400: '#facc15',
          500: '#eab308',
          600: '#ca8a04',
          700: '#a16207',
          800: '#854d0e',
          900: '#713f12',
        }
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif'],
        display: ['Poppins', 'system-ui', 'sans-serif'],
      },
      spacing: {
        '18': '4.5rem',
        '88': '22rem',
        '128': '32rem',
      },
      borderRadius: {
        '4xl': '2rem',
      },
      animation: {
        'fade-in': 'fadeIn 0.5s ease-in-out',
        'slide-up': 'slideUp 0.3s ease-out',
        'bounce-gentle': 'bounceGentle 2s infinite',
      },
      keyframes: {
        fadeIn: {
          '0%': { opacity: '0' },
          '100%': { opacity: '1' },
        },
        slideUp: {
          '0%': { transform: 'translateY(10px)', opacity: '0' },
          '100%': { transform: 'translateY(0)', opacity: '1' },
        },
        bounceGentle: {
          '0%, 100%': { transform: 'translateY(0)' },
          '50%': { transform: 'translateY(-5px)' },
        },
      },
    },
  },
  plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/typography'),
    require('@tailwindcss/aspect-ratio'),
  ],
};

Classes personnalisées avec @apply

css
/* styles/components.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  .btn-primary {
    @apply px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 transition-colors;
  }

  .btn-secondary {
    @apply px-6 py-3 bg-gray-600 text-white rounded-lg hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors;
  }

  .btn-outline {
    @apply px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 transition-colors;
  }

  .card {
    @apply bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300;
  }

  .input-field {
    @apply w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-colors;
  }

  .form-label {
    @apply block text-sm font-medium text-gray-700 mb-2;
  }

  .section-title {
    @apply text-3xl font-bold text-gray-900 mb-6;
  }

  .section-subtitle {
    @apply text-lg text-gray-600 mb-8;
  }
}

Utilisation des classes personnalisées

tsx
// Utilisation des classes personnalisées
function EventForm() {
  return (
    <form className="space-y-6">
      <div>
        <label htmlFor="title" className="form-label">
          Titre de l'événement *
        </label>
        <input
          type="text"
          id="title"
          className="input-field"
          placeholder="Ex: Match d'improvisation"
        />
      </div>

      <div>
        <label htmlFor="description" className="form-label">
          Description *
        </label>
        <textarea
          id="description"
          rows={4}
          className="input-field resize-vertical"
          placeholder="Décrivez votre événement..."
        />
      </div>

      <div className="flex justify-end gap-4">
        <button type="button" className="btn-outline">
          Réinitialiser
        </button>
        
        <button type="submit" className="btn-primary">
          Créer l'événement
        </button>
      </div>
    </form>
  );
}

function EventList() {
  return (
    <div>
      <h2 className="section-title">Nos spectacles</h2>
      <p className="section-subtitle">
        Découvrez tous nos événements d'improvisation
      </p>
      
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {/* Cartes d'événements */}
      </div>
    </div>
  );
}

🚀 Optimisations et bonnes pratiques

Purge CSS en production

javascript
// tailwind.config.js
module.exports = {
  content: [
    './app/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  // En production, TailwindCSS supprime automatiquement les classes non utilisées
  // pour réduire la taille du CSS final
}

Organisation des classes

tsx
// ❌ Classes mélangées et difficiles à lire
<div className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300 cursor-pointer p-6 border border-gray-200">

// ✅ Classes organisées par catégorie
<div className="
  /* Layout */
  p-6
  
  /* Apparence */
  bg-white 
  border 
  border-gray-200 
  rounded-lg 
  shadow-md 
  overflow-hidden
  
  /* Interactions */
  hover:shadow-lg 
  cursor-pointer
  
  /* Animations */
  transition-shadow 
  duration-300
">

Composants avec variantes

tsx
interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'outline';
  size?: 'sm' | 'md' | 'lg';
  children: React.ReactNode;
  className?: string;
}

function Button({ variant = 'primary', size = 'md', children, className = '' }: ButtonProps) {
  const baseClasses = 'font-medium rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors';
  
  const variantClasses = {
    primary: 'bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500',
    secondary: 'bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500',
    outline: 'border border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-primary-500'
  };
  
  const sizeClasses = {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2 text-sm',
    lg: 'px-6 py-3 text-base'
  };

  return (
    <button
      className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
    >
      {children}
    </button>
  );
}

// Utilisation
<Button variant="primary" size="lg">
  Créer l'événement
</Button>

<Button variant="outline" size="sm">
  Annuler
</Button>

🧪 Tests et qualité

Tests des composants stylisés

tsx
// __tests__/components/EventCard.test.tsx
import { render, screen } from '@testing-library/react';
import { EventCard } from '@/components/EventCard';

describe('EventCard', () => {
  const mockEvent = {
    id: '1',
    title: 'Match d\'improvisation',
    description: 'Un spectacle passionnant',
    date: '2024-12-25T20:00:00Z',
    maxPlayers: 12
  };

  it('should render with correct TailwindCSS classes', () => {
    render(<EventCard event={mockEvent} />);
    
    const card = screen.getByRole('article');
    
    // Vérifier les classes TailwindCSS
    expect(card).toHaveClass('bg-white');
    expect(card).toHaveClass('rounded-lg');
    expect(card).toHaveClass('shadow-md');
    expect(card).toHaveClass('hover:shadow-lg');
    expect(card).toHaveClass('transition-shadow');
  });

  it('should be responsive', () => {
    render(<EventCard event={mockEvent} />);
    
    const card = screen.getByRole('article');
    
    // Vérifier les classes responsive
    expect(card).toHaveClass('md:grid-cols-2');
    expect(card).toHaveClass('lg:grid-cols-3');
  });
});

🚀 Bonnes pratiques

Do's

  • Organiser les classes par catégorie pour la lisibilité
  • Utiliser les classes utilitaires au lieu du CSS personnalisé
  • Créer des composants réutilisables avec des variantes
  • Utiliser le système responsive pour l'adaptabilité
  • Personnaliser le design system avec la configuration

Don'ts

  • Mélanger les classes sans organisation
  • Dupliquer les styles - Créer des composants réutilisables
  • Ignorer la responsivité - Toujours penser mobile-first
  • Abuser des classes personnalisées - Préférer les utilitaires

🚀 Prochaines étapes


TailwindCSS est notre système de design. Maîtrisez-le pour créer des interfaces cohérentes et responsive ! 🎨

Released under the MIT License.