Next.js + Tailwind CSS : bonnes pratiques — guide complet de la config au mode sombre (2025)

Regardez le className de ce bouton dans VS Code — vingt-trois classes. De bg-blue-500 à dark:hover:bg-blue-800, tout tassé sur une ligne, barre de défilement horizontal interminable. Un collègue passe devant mon poste, jette un œil à l’écran : « Mais qu’est-ce que c’est que ça ? »
À ce moment-là, je ne savais pas quoi répondre. J’utilise Tailwind depuis presque deux ans, c’est rapide, oui, mais le code ressemble de plus en plus à du charabia. Copier-coller, c’est tentant ; modifier ensuite, c’est l’enfer — pour uniformiser les coins arrondis de tous les boutons, il faut fouiller rounded-lg fichier par fichier.
Ce n’est pas qu’un problème personnel. En 2025, Tailwind CSS est passé en v4, Next.js en 15 : config, mode sombre, performance — tout a changé. Au début, j’étais perdu. Plus de fichier de config ? Où est passé darkMode: 'class' ? Après pas mal de galères, j’ai fini par comprendre.
Cet article partage ce que j’ai appris sur le terrain. Pas un « relecteur de doc officielle », mais des méthodes testées en vrai projet : éviter l’explosion des classes, gérer le mode sombre proprement, faire passer le CSS de 500 Ko à 50 Ko. Si Tailwind vous a déjà agacé ou si vous hésitez à passer en v4, continuez la lecture.
Les nouveautés 2025 : Tailwind CSS v4 + Next.js 15
Commençons par le plus gros changement de v4 — le fichier de config a disparu.
Oui, le fameux tailwind.config.js est devenu optionnel en v4. La première fois, j’ai cru à une blague. J’ouvre un nouveau projet Next.js 15.3 : rien. L’équipe Tailwind parle de « philosophie zéro config » : scan automatique des fichiers, prêt à l’emploi.
Ça ne veut pas dire qu’on ne peut plus personnaliser. Au contraire, v4 déplace tout dans un endroit plus intuitif — global.css. Couleurs, espacements, polices : tout se définit en variables CSS :
@theme {
--color-primary: #3b82f6;
--color-secondary: #8b5cf6;
--font-sans: 'Inter', sans-serif;
}
Au début, j’étais sceptique : « C’est un retour en arrière ? » Deux jours plus tard, je constatais que c’était plus rapide à modifier. Avant, changer une couleur de thème impliquait de redémarrer le serveur de dev ; maintenant, une variable CSS et le hot reload réagit tout de suite. Les designers comprennent aussi les variables CSS, sans me demander « c’est quel bleu, blue-500 ? »
Autre gain concret : la vitesse. v4 est réécrit en Rust ; l’équipe annonce ~5× plus rapide. Mon test : cold start de 8 s à moins de 2 s. Ce n’est pas du benchmark pour le plaisir — quand on lance le serveur une dizaine de fois par jour, ça vaut deux cafés de gagnés.
Côté Next.js 15, l’App Router est la norme. Avec Tailwind, l’isolation des styles des Server Components est solide, pas de pollution CSS. Attention : 'use client' sur les composants clients, sinon la bascule de thème pose problème (on y reviendra).
Petit détail souvent oublié : en v4, border-color par défaut est currentColor. Sans couleur explicite, la bordure suit le texte. En migration depuis v3, des bordures peuvent sembler « disparaître » — elles prennent juste la couleur du texte. Je suis tombé dedans et j’ai cherché longtemps.
En bref, v4 change beaucoup, mais dans le bon sens : plus rapide, plus simple, plus intuitif. Après la phase d’adaptation, difficile de revenir en arrière.
Classes trop longues : la bonne approche par encapsulation
Revenons au bouton avec vingt classes : que faire ?
Beaucoup pensent à @apply. On met les classes Tailwind dans un fichier CSS, on nomme .btn-primary, ça paraît plus propre. Je l’ai fait, jusqu’à lire Adam Wathan sur Twitter : « Si vous utilisez @apply partout, vous avez peut-être mal compris Tailwind. »
Ça pique, mais c’est vrai. @apply compile les utilitaires dans le CSS final ; l’avantage « à la demande » de Tailwind disparaît. On croit « encapsuler », on gonfle le bundle — sur un projet, le CSS prod est passé de 30 Ko à 120 Ko à force de @apply.
La bonne approche ? L’encapsulation en composants.
Regrouper les combinaisons courantes dans des composants React : les classes restent nombreuses, mais une seule fois :
// ❌ Avant : répéter partout
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg shadow-md transition duration-200">
Envoyer
</button>
// ✅ Maintenant : composant
<Button variant="primary">Envoyer</Button>
Dans Button, on garde toutes les classes ; ailleurs, c’est propre. Pour changer le style global, un seul fichier.
Ce n’est pas suffisant. Boutons primaires, secondaires, danger… un composant par variante ? Entre cva (class-variance-authority).
Cette lib gère les variantes, très bien avec Tailwind :
import { cva, type VariantProps } from 'class-variance-authority'
const buttonStyles = cva(
// Styles de base
'font-bold rounded-lg transition duration-200',
{
variants: {
variant: {
primary: 'bg-blue-500 hover:bg-blue-700 text-white',
secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-800',
danger: 'bg-red-500 hover:bg-red-700 text-white'
},
size: {
sm: 'py-1 px-3 text-sm',
md: 'py-2 px-4',
lg: 'py-3 px-6 text-lg'
}
},
defaultVariants: {
variant: 'primary',
size: 'md'
}
}
)
export function Button({
variant,
size,
children,
...props
}: VariantProps<typeof buttonStyles> & React.ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button className={buttonStyles({ variant, size })} {...props}>
{children}
</button>
)
}
Usage :
<Button variant="primary">Enregistrer</Button>
<Button variant="danger" size="lg">Supprimer</Button>
<Button variant="secondary" size="sm">Annuler</Button>
TypeScript vérifie les variantes ; une faute de frappe, erreur à la compile. shadcn/ui fonctionne ainsi — code très lisible.
@apply n’est pas interdit : pour surcharger une lib tierce sans composant, OK. Pour votre code, encapsulez plutôt que de tricher.
Thème personnalisé : construire votre design system
Composants encapsulés — prochaine question : comment garder un style cohérent ?
Sur d’anciens projets, j’avais cinq bleus différents : blue-400, blue-500, #3B82F6, rgb(59, 130, 246)… Le designer secouait la tête. Il faut un design system : couleurs, typo, espacements figés.
En v4, c’est simple avec @theme :
/* app/globals.css */
@import 'tailwindcss';
@theme {
/* Couleurs de marque */
--color-brand-primary: #3b82f6;
--color-brand-secondary: #8b5cf6;
/* Couleurs sémantiques */
--color-success: #10b981;
--color-warning: #f59e0b;
--color-error: #ef4444;
/* Neutres (clair → foncé) */
--color-neutral-50: #f9fafb;
--color-neutral-100: #f3f4f6;
--color-neutral-500: #6b7280;
--color-neutral-900: #111827;
/* Polices */
--font-sans: 'Inter', system-ui, sans-serif;
--font-mono: 'Fira Code', monospace;
/* Espacement (grille 8 px) */
--spacing-unit: 0.5rem; /* 8px */
/* Rayons */
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--radius-lg: 1rem;
}
Puis dans les classes Tailwind :
<div className="bg-brand-primary text-neutral-50 rounded-md">
Fond couleur de marque
</div>
Pas bg-blue-500, mais bg-brand-primary. Changer la marque ? Une variable, tout le site suit. Fini le grep sur blue-500 cent fois.
Vous pouvez garder tailwind.config.ts style v3 :
import type { Config } from 'tailwindcss'
export default {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
// Étendre le thème par défaut (recommandé)
colors: {
brand: {
primary: '#3b82f6',
secondary: '#8b5cf6',
},
},
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
},
},
} satisfies Config
extend est crucial. theme.colors seul écrase toutes les couleurs Tailwind — plus de bg-red-500. extend ajoute sans remplacer.
Astuce : variables CSS + config Tailwind pour changer de thème à l’exécution :
:root {
--color-primary: #3b82f6;
}
[data-theme='purple'] {
--color-primary: #8b5cf6;
}
// tailwind.config.ts
colors: {
primary: 'var(--color-primary)',
}
Changement de thème sans recompiler le CSS — un attribut DOM suffit. Très utile en SaaS pour la couleur choisie par l’utilisateur.
Avec un design system, l’équipe avance mieux. Un nouveau dev lit globals.css et sait quelles couleurs utiliser — fini le « bleu au pif ».
Mode sombre : la solution sans scintillement
Mon plus gros échec sur le mode sombre.
Première fois : tuto v3, bouton de bascule, clic — flash blanc puis noir. « Ça brûle les yeux », disaient les utilisateurs. Flash of unstyled content (FOUC), classique en SSR Next.js.
En v4, plus de darkMode: 'class' à configurer — stratégie par classe par défaut. Le flash reste ; next-themes règle ça.
Installation :
npm install next-themes
Enveloppez la racine avec ThemeProvider :
// app/layout.tsx
import { ThemeProvider } from 'next-themes'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="fr" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
</body>
</html>
)
}
suppressHydrationWarning est indispensable. next-themes ajoute class="dark" côté client sur <html> ; sans cet attribut, React alerte sur le mismatch SSR.
Bouton de bascule en composant client :
'use client'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
export function ThemeToggle() {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
useEffect(() => setMounted(true), [])
if (!mounted) return null // Évite le mismatch SSR
return (
<button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className="rounded-lg p-2 hover:bg-neutral-100 dark:hover:bg-neutral-800"
>
{theme === 'dark' ? '🌞' : '🌙'}
</button>
)
}
Variables CSS pour le mode sombre :
@theme {
--color-bg-primary: #ffffff;
--color-text-primary: #111827;
}
.dark {
--color-bg-primary: #111827;
--color-text-primary: #f9fafb;
}
Ou le préfixe dark: :
<div className="bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-50">
Mode sombre adaptatif
</div>
Le mode sombre n’est pas un simple inversion noir/blanc. Le noir pur #000000 agresse ; préférez un gris foncé #111827 ou #1a1a1a. Texte blanc pur trop vif — #f9fafb est plus confortable. Les ombres : en sombre, inverser la logique ou utiliser ring :
// Mode clair : ombre vers le bas
<div className="shadow-lg dark:shadow-none dark:ring-1 dark:ring-neutral-800">
En sombre, ring remplace souvent l’ombre pour la profondeur.
Images trop lumineuses en sombre :
.dark img {
filter: brightness(0.9);
}
Ces détails font un mode sombre vraiment utilisable, pas juste « coché dans la todo ».
Performance : CSS léger et rapide
Le passage de 500 Ko à 50 Ko, ce n’est pas du bluff — je l’ai fait.
Le JIT v4 est activé par défaut. Le levier principal reste content :
// ❌ Portée trop large
content: [
'./**/*.{js,ts,jsx,tsx}',
]
Ça scanne tout, y compris node_modules et .next. Soyez précis :
// ✅ Répertoires utiles seulement
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx}',
'./lib/**/*.{js,ts}',
]
Chez moi, le démarrage du serveur de dev a gagné ~40 %.
Piège classique : classes dynamiques :
// ❌ Purge ne les voit pas
const colors = ['red', 'blue', 'green']
<div className={`bg-${colors[0]}-500`}>
Tailwind ne voit pas bg-red-500 complet ; en prod, styles absents. Écrivez les classes complètes :
// ✅ Classes complètes
const colorMap = {
red: 'bg-red-500',
blue: 'bg-blue-500',
green: 'bg-green-500',
}
<div className={colorMap[color]}>
Ou safelist :
// tailwind.config.ts
safelist: [
{
pattern: /bg-(red|blue|green)-500/,
},
]
N’en abusez pas — le CSS regrossit. Préférez les classes explicites dans le code.
v4 minifie le CSS en prod. Pour aller plus loin, cssnano :
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}),
},
}
Surveillez la taille du bundle avec @next/bundle-analyzer :
npm install --save-dev @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({
// Autres options
})
Puis ANALYZE=true npm run build pour un rapport visuel.
Cas Netflix : leur page Top 10, ~6,5 Ko de CSS. Extrême, mais l’idée tient : ne garder que le nécessaire.
En review, un collègue importait tout @heroicons/react pour deux icônes. Import ciblé : −200 Ko. Petit geste, gros effet cumulé.
L’optimisation performance est continue. Analyse bundle avant chaque release — le CSS reste sous contrôle.
Migration v3 → v4 : monter en douceur
Encore en v3 ? Faut-il migrer maintenant ? Ça dépend.
Nouveau projet : v4 sans hésiter. Projet existant : estimez le coût. v4 apporte des breaking changes — pas un simple npm install.
Gros changement : la config. Le contenu de tailwind.config.js v3 va dans global.css :
/* Avant dans tailwind.config.js */
module.exports = {
theme: {
extend: {
colors: {
primary: '#3b82f6',
},
},
},
}
/* Maintenant dans globals.css */
@theme {
--color-primary: #3b82f6;
}
Utilitaires custom : @layer utilities → @utility :
/* v3 */
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
/* v4 */
@utility text-balance {
text-wrap: balance;
}
Piège discret : plus de variantes sur les classes composant. En v3 :
/* v3 OK */
@layer components {
.btn {
@apply px-4 py-2 rounded;
}
}
/* Puis hover:btn, dark:btn… */
En v4, hover:btn échoue. Passez par une utility ou un composant React.
Bordures : v4 utilise currentColor par défaut. Cherchez border et ajoutez border-neutral-300 où il manque une couleur :
// v3 : bordure grise implicite
<div className="border"></div>
// v4 : couleur explicite
<div className="border border-neutral-300"></div>
Migration par étapes :
- Étape 1 : installer v4, lancer le dev, repérer les régressions visuelles
- Étape 2 : remplacer
@layerpar@utilityou@theme - Étape 3 : compléter les
bordersans couleur - Étape 4 : déplacer
themevers le CSS, tester au fil de l’eau - Étape 5 :
safelistsi des classes dynamiques disparaissent
Comptez une demi-journée à une journée selon la taille du projet. Par petits lots, rollback facile.
Si vous utilisez shadcn/ui ou autre lib, vérifiez la compatibilité v4. J’ai migré avant la lib — styles cassés partout.
v4 vaut le coup, mais ce n’est pas urgent. v3 stable ? Attendez le bon moment. La dette technique se paie quand ça arrange.
Conclusion
Du bouton à 23 classes à <Button variant="primary">, presque deux ans de chemin.
Tailwind + Next.js, c’est puissant, mais pas plug-and-play. v4 impressionne au début ; en pratique, c’est plus simple, plus rapide, meilleure DX.
Encapsulation, thème, mode sombre, perf — pas besoin de tout appliquer d’un coup. Prenez ce qui soulage votre pain actuel. Pas de refonte globale d’un coup : risque et fatigue.
Commencez par l’encapsulation. Une après-midi pour Button, Card, Input + cva : faible risque, gros gain immédiat. Puis mode sombre et thème.
Pour v4 : regardez l’écosystème, vos libs, votre disponibilité. Le plus récent n’est pas toujours le plus adapté.
Vous avez d’autres astuces Tailwind ? Partagez en commentaire — peut-être plus élégantes que les miennes.
Les exemples de code sont sur GitHub (lien en bas d’article). Questions : ouvrez une Issue.
Ne vous contentez pas de lire — lancez le code. On n’apprend qu’en faisant tourner.
Configuration complète Next.js + Tailwind CSS v4
De la config de base à l'encapsulation, la performance et le mode sombre
⏱️ Estimated time: 3 hr
- 1
Step 1: Configuration de base Tailwind v4
Changements v4 :
• Fichier de config optionnel (zéro config)
• Personnalisation dans global.css via variables CSS
• Moteur Rust ~5× plus rapide
Configurer global.css :
```css
@theme {
--color-primary: #3b82f6;
--color-secondary: #8b5cf6;
--font-sans: 'Inter', sans-serif;
}
```
Avantages :
• Modifier une variable CSS = hot reload immédiat
• Compréhensible pour les designers
• Pas de redémarrage du serveur de dev
Point clé : philosophie zéro config, scan automatique des fichiers du projet. - 2
Step 2: Résoudre l'explosion des classes
Problème : classes trop longues, 23 noms sur une ligne.
Solution : encapsulation en composants
Gérer les variantes avec cva :
```tsx
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground',
destructive: 'bg-destructive text-destructive-foreground',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 px-3',
lg: 'h-11 px-8',
},
},
}
)
export function Button({ variant, size, className, ...props }) {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
)
}
```
Résultat : de 23 classes à 3 (variant, size, className)
Point clé : une après-midi pour Button, Card, Input + cva. - 3
Step 3: Performance (500 Ko → 50 Ko)
Méthodes :
1. Config purge / content :
```js
// tailwind.config.js (v3)
module.exports = {
content: ['./app/**/*.{js,ts,jsx,tsx}'],
// Seulement les classes utilisées
}
```
2. Imports ciblés :
```tsx
// Ne pas importer toute la lib
import { Button } from '@/components/ui/button'
```
3. Éviter les classes dynamiques :
```tsx
// ❌ Erreur : purge ne détecte pas
const color = `bg-${theme}-500`
// ✅ Correct : classes complètes
const color = theme === 'blue' ? 'bg-blue-500' : 'bg-red-500'
```
4. Mode JIT (v3) :
```js
module.exports = {
mode: 'jit', // Génération à la demande
}
```
Résultat : 500 Ko → 50 Ko (−90 %) - 4
Step 4: Configuration du mode sombre
v4 : plus de darkMode, utiliser next-themes :
Installation :
```bash
npm install next-themes
```
Configurer ThemeProvider :
```tsx
'use client'
import { ThemeProvider } from 'next-themes'
export function Providers({ children }) {
return (
<ThemeProvider attribute="class" defaultTheme="system">
{children}
</ThemeProvider>
)
}
```
Préfixe dark: :
```tsx
<div className="bg-white dark:bg-gray-900 text-black dark:text-white">
Contenu
</div>
```
Points clés :
• v4 supporte dark: nativement
• next-themes pour gérer le thème
• Pas de config darkMode
FAQ
Quels changements dans Tailwind v4 ?
1. Fichier de config optionnel (zéro config)
• tailwind.config.js devient optionnel
• Scan automatique des fichiers du projet
2. Personnalisation dans global.css
• Variables CSS pour couleurs, espacements, polices
• Hot reload immédiat en modifiant les variables
3. Moteur réécrit en Rust
• ~5× plus rapide
• Cold start de 8 s à moins de 2 s
4. Mode sombre simplifié
• Plus de config darkMode
• Préfixe dark: natif
Avantages :
• Config plus simple
• Plus rapide
• Hot reload amélioré
Note : v4 était en beta ; en prod, attendez une version stable.
Comment éviter l'explosion des classes ?
Solution : encapsulation en composants
Variantes avec cva :
```tsx
import { cva } from 'class-variance-authority'
const buttonVariants = cva(
'inline-flex items-center justify-center',
{
variants: {
variant: {
default: 'bg-primary',
destructive: 'bg-destructive',
},
size: {
default: 'h-10 px-4',
sm: 'h-9 px-3',
},
},
}
)
export function Button({ variant, size, className, ...props }) {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
)
}
```
Effet :
• De 23 classes à 3
• Variantes centralisées
• Modifications faciles
Conseil : une après-midi pour Button, Card, Input + cva.
Comment optimiser les perfs Tailwind (500 Ko → 50 Ko) ?
1. Config content / purge :
```js
module.exports = {
content: ['./app/**/*.{js,ts,jsx,tsx}'],
}
```
2. Imports ciblés :
```tsx
import { Button } from '@/components/ui/button'
```
3. Éviter les classes dynamiques :
```tsx
// ❌ Erreur
const color = `bg-${theme}-500`
// ✅ Correct
const color = theme === 'blue' ? 'bg-blue-500' : 'bg-red-500'
```
4. Mode JIT (v3) :
```js
module.exports = {
mode: 'jit',
}
```
Résultat : 500 Ko → 50 Ko (−90 %)
Point clé : n'inclure que les classes réellement utilisées, éviter le dynamique.
Comment configurer le mode sombre en Tailwind v4 ?
Installation :
```bash
npm install next-themes
```
Configuration :
```tsx
'use client'
import { ThemeProvider } from 'next-themes'
export function Providers({ children }) {
return (
<ThemeProvider attribute="class" defaultTheme="system">
{children}
</ThemeProvider>
)
}
```
Usage :
```tsx
<div className="bg-white dark:bg-gray-900">
Contenu
</div>
```
Points clés :
• v4 supporte dark: nativement
• next-themes pour le thème
• Pas de config darkMode
Note : ajouter suppressHydrationWarning sur la balise html.
Faut-il migrer vers Tailwind v4 ?
Avantages :
• ~5× plus rapide
• Config plus simple
• Hot reload amélioré
Inconvénients :
• Était en beta
• Écosystème parfois immature
• Migration chronophage
Conseils :
• Nouveau projet : v4 envisageable
• Projet existant : attendre une version stable
• Incertain : rester en v3
Point clé : le plus récent n'est pas toujours le plus adapté. Vérifiez l'écosystème, vos libs, votre disponibilité.
Comment gérer le thème Tailwind ?
Définir dans global.css :
```css
@theme {
--color-primary: #3b82f6;
--color-secondary: #8b5cf6;
--font-sans: 'Inter', sans-serif;
}
```
Usage :
```tsx
<div className="bg-primary text-primary-foreground">
Contenu
</div>
```
Avantages :
• Hot reload en modifiant les variables
• Lisible pour les designers
• Pas de redémarrage du serveur
Conseil :
• Commencer par l'encapsulation
• Une après-midi pour Button, Card, Input
• cva pour les variantes
• Puis personnalisation du thème
11 min de lecture · Publié le: 20 déc. 2025 · Mis à jour le: 27 juil. 2026
Guide complet Next.js
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Mode sombre Next.js : guide complet next-themes
Du scintillement au zéro flash : implémentez le mode sombre Next.js avec next-themes. Code complet, explications et dépannage des problèmes courants.
Partie 44 sur 51
Suivant
Next.js App Router + shadcn/ui : guide pour mélanger Server et Client Components
Guide pratique pour bien combiner Server Components et Client Components dans Next.js App Router : intégration shadcn/ui, conception du flux de données, correction des erreurs courantes et optimisation des performances
Partie 46 sur 51



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire