Patterns Avancés
@apply, arbitrary values, animations et composition.
8.1@apply - Extraire des classes
Quand tu répètes le même groupe de classes, @apply permet de créer des classes CSS custom :
CSScss
/* styles/globals.css */
@import "tailwindcss";
@layer components {
.btn {
@apply inline-flex items-center justify-center gap-2
px-5 py-2.5 rounded-lg font-medium
transition-colors focus-visible:outline-none
focus-visible:ring-2 focus-visible:ring-offset-2;
}
.btn-primary {
@apply btn bg-blue-600 text-white
hover:bg-blue-700 active:bg-blue-800
focus-visible:ring-blue-500;
}
.btn-secondary {
@apply btn border-2 border-slate-300 text-slate-700
hover:bg-slate-50 active:bg-slate-100
focus-visible:ring-slate-500;
}
.input {
@apply w-full px-4 py-2.5 rounded-lg border border-slate-300
text-slate-900 placeholder:text-slate-400
focus:outline-none focus:ring-2 focus:ring-blue-500
focus:border-blue-500 transition-colors;
}
.card {
@apply bg-white rounded-xl border border-slate-200
shadow-sm hover:shadow-md transition-shadow;
}
}HTMLhtml
<!-- Avant @apply (répétitif) -->
<button class="inline-flex items-center justify-center gap-2
px-5 py-2.5 rounded-lg font-medium bg-blue-600
text-white hover:bg-blue-700 ...">
Sauvegarder
</button>
<!-- Après @apply (propre) -->
<button class="btn-primary">Sauvegarder</button>
<button class="btn-secondary">Annuler</button>
<input class="input" placeholder="Email" />8.2Valeurs arbitraires
Quand l'échelle prédéfinie ne suffit pas, utilise les crochets [] :
HTMLhtml
<!-- Valeurs arbitraires avec [] -->
<div class="w-[350px]"> Largeur exacte de 350px
<div class="h-[calc(100vh-64px)]"> Hauteur viewport moins header
<div class="top-[117px]"> Position top custom
<div class="bg-[#1da1f2]"> Couleur Twitter exacte
<div class="text-[13px]"> Taille de texte custom
<div class="grid-cols-[200px_1fr_200px]"> Grille custom
<div class="p-[clamp(1rem,3vw,2rem)]"> Padding fluide
<!-- Propriétés CSS arbitraires -->
<div class="[mask-image:linear-gradient(to_bottom,black,transparent)]">
Masque CSS avancé
</div>
<!-- Combiné avec des modificateurs -->
<div class="hover:bg-[#1da1f2] md:w-[600px]">
Arbitraire + hover + responsive
</div>8.3Composants React + Tailwind
En React/Next.js, on crée des composants réutilisables plutôt que d'utiliser @apply :
TSXtsx
// components/ui/button.tsx
import { cn } from "@/lib/utils";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "ghost";
size?: "sm" | "md" | "lg";
}
export function Button({
variant = "primary",
size = "md",
className,
children,
...props
}: ButtonProps) {
return (
<button
className={cn(
// Base
"inline-flex items-center justify-center gap-2 rounded-lg",
"font-medium transition-colors focus-visible:outline-none",
"focus-visible:ring-2 focus-visible:ring-offset-2",
// Variants
variant === "primary" && "bg-blue-600 text-white hover:bg-blue-700",
variant === "secondary" && "border-2 border-slate-300 text-slate-700 hover:bg-slate-50",
variant === "ghost" && "text-slate-600 hover:bg-slate-100",
// Sizes
size === "sm" && "px-3 py-1.5 text-sm",
size === "md" && "px-5 py-2.5 text-sm",
size === "lg" && "px-7 py-3 text-base",
// Custom classes
className
)}
{...props}
>
{children}
</button>
);
}TSXtsx
// Utilisation dans un composant
import { Button } from "@/components/ui/button";
export function Hero() {
return (
<div className="flex gap-3">
<Button variant="primary" size="lg">
Commencer
</Button>
<Button variant="secondary" size="lg">
En savoir plus
</Button>
</div>
);
}8.4Animations custom
CSScss
/* Animations custom dans globals.css (v4) */
@import "tailwindcss";
@theme {
--animate-slide-up: slide-up 0.3s ease-out;
--animate-slide-down: slide-down 0.3s ease-out;
--animate-scale-in: scale-in 0.2s ease-out;
--animate-fade-in-up: fade-in-up 0.5s ease-out;
}
@keyframes slide-up {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
@keyframes slide-down {
from { transform: translateY(-10px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes scale-in {
from { transform: scale(0.95); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
@keyframes fade-in-up {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}HTMLhtml
<!-- Utilisation -->
<div class="animate-slide-up">Modal qui glisse</div>
<div class="animate-scale-in">Popup qui scale</div>
<div class="animate-fade-in-up">Contenu qui apparaît</div>
<!-- Animation avec délai -->
<div class="animate-fade-in-up [animation-delay:200ms]">
Apparaît après 200ms
</div>
<!-- Contrôle du mouvement (accessibilité) -->
<div class="motion-safe:animate-fade-in-up motion-reduce:opacity-100">
Respecte les préférences de mouvement réduit
</div>8.5Composition de variantes
HTMLhtml
<!-- Empiler les modificateurs -->
<div class="dark:md:hover:bg-blue-600">
<!-- ↑ Dark mode + desktop + hover -->
Bleu au hover sur desktop en dark mode
</div>
<!-- Group avec variantes nommées -->
<div class="group/card hover:shadow-lg">
<div class="group/icon inline-block">
<i class="group-hover/icon:text-blue-500 fa-solid fa-star"></i>
</div>
<h3 class="group-hover/card:text-blue-600">Titre</h3>
</div>
<!-- Data attributes -->
<div data-state="open" class="data-[state=open]:bg-blue-50
data-[state=closed]:hidden">
Contenu conditionnel par data attribute
</div>
<!-- Aria states -->
<button aria-expanded="true"
class="aria-expanded:bg-blue-50 aria-expanded:text-blue-700">
Menu
</button>