🎨 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
- React Fundamentals - Bases de React 18
- Next.js - App Router et composants
- Frontend Overview - Vue d'ensemble du frontend
- Backend - PayloadCMS et architecture
TailwindCSS est notre système de design. Maîtrisez-le pour créer des interfaces cohérentes et responsive ! 🎨
