Créer une section pricing 3 colonnes en React avec plan recommandé
Une section pricing React affiche 2 à 4 objets tier dans une grille CSS et utilise un booléen `highlighted` pour inverser la carte en couleur d'accent, permutant les tokens fond/texte pour que le plan recommandé ressorte sans balisage supplémentaire. Framer Motion anime l'entrée de chaque carte au scroll avec un délai échelonné.
- Stack : React 18 + Framer Motion 11 + Tailwind v4 + lucide-react, ~195 lignes au total.
- Animation : fade-up whileInView avec stagger (délai i * 0.1s) et un ease spring custom [0.16, 1, 0.3, 1].
- Thème : 100% CSS custom properties, zéro couleur en dur, compatible avec les 7 presets.
- Accessible : h3 sémantique par formule, icône Check décorative, éléments button pour les CTAs.
- Responsive : colonne unique sur mobile, grille 3 colonnes à partir du breakpoint md.
Pricing Cards est une section tarifs responsive à 3 formules où une carte bascule sur un fond plein accent pour signaler le plan recommandé. L'en-tête remonte au scroll, puis chaque carte suit avec un fade-up échelonné. Suffisamment subtil pour paraître soigné, suffisamment rapide pour ne pas bloquer la conversion. Le tout tourne sur des CSS tokens, donc il s'adapte à n'importe quel preset de couleurs sans toucher au composant.
Anatomie
Le composant a deux zones. En haut, un bloc d'en-tête centré contient un badge uppercase optionnel (couleur accent), un titre h2 et un sous-titre optionnel. En dessous, un `grid md:grid-cols-3` dispose les cartes de formule. Chaque carte est un div `flex flex-col rounded-xl p-8` : nom, description, prix, liste de features flex-1 avec icônes Check, puis bouton CTA pleine largeur épinglé en bas. Quand `highlighted` est true, la carte bascule son fond sur `--color-accent`, tous les tokens texte sur `--color-background`, et un badge pill 'Populaire' apparaît au-dessus du bord supérieur via positionnement absolu.
Comment ça marche
L'en-tête et les cartes utilisent le `whileInView` de Framer Motion avec `viewport={{ once: true }}` pour que l'animation se déclenche une seule fois quand la section entre dans le viewport. L'en-tête fait un simple `{ opacity: 0, y: 20 }` vers `{ opacity: 1, y: 0 }` en 0.6s. Chaque carte fait le même fade-up avec `delay: i * 0.1` calculé depuis l'index du tableau, créant une cascade de gauche à droite. Les deux partagent le même cubic bezier custom `[0.16, 1, 0.3, 1]`, une courbe fast-out-slow-in qui donne du poids à l'entrée sans traîner.
Comment le coder en React
Définir l'interface Tier et les props
Crée une interface `Tier` avec `name`, `price`, `currency`, `period`, `description`, `features`, `highlighted` et `ctaLabel`. Le composant reçoit `badge`, `title`, `subtitle` et `tiers` en props optionnelles avec des valeurs par défaut. Garder les données à l'extérieur permet d'utiliser le même composant pour tous les produits.
interface Tier { name: string; price: number; currency: string; period: string; description: string; features: string[]; highlighted: boolean; ctaLabel: string; }Animer l'en-tête au scroll
Enveloppe le bloc d'en-tête dans un `motion.div` avec `initial={{ opacity: 0, y: 20 }}`, `whileInView={{ opacity: 1, y: 0 }}` et `viewport={{ once: true }}`. Utilise l'ease spring custom pour qu'il entre proprement. L'en-tête se déclenche en premier, avant les cartes, donnant à la section une entrée en deux temps.
const ease: [number, number, number, number] = [0.16, 1, 0.3, 1]; <motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} transition={{ duration: 0.6, ease }} viewport={{ once: true }} >Échelonner les cartes par index
Itère sur `tiers` et enveloppe chaque carte dans un `motion.div` avec `delay: i * 0.1`. La première carte entre à 0ms, la deuxième à 100ms, la troisième à 200ms. Garde la durée à 0.5s, plus courte que l'en-tête pour un rendu plus vif.
{tiers.map((tier, i) => ( <motion.div key={tier.name} initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, ease, delay: i * 0.1 }} viewport={{ once: true }} >Inverser la carte mise en avant avec les CSS tokens
Utilise le booléen `highlighted` pour permuter les styles inline fond et texte. Le fond de la carte passe à `var(--color-accent)`, tout le texte bascule sur `var(--color-background)`, et les icônes Check aussi. Pas de classe CSS supplémentaire. Le badge 'Populaire' est positionné en absolu avec un offset top négatif pour chevaucher la bordure de la carte.
style={{ background: tier.highlighted ? "var(--color-accent)" : "var(--color-background-card)", border: tier.highlighted ? "none" : "1px solid var(--color-border)", }}
Quand l'utiliser
À utiliser sur les landing SaaS, agences ou produits où il faut un moment de conversion propre après la section features. Le layout 3 colonnes convient parfaitement aux formules Gratuit/Pro/Entreprise. À éviter si tu as plus de 4 plans, au-delà, un tableau comparatif est plus lisible. Évite-le aussi si tu as besoin d'un toggle mensuel/annuel ; ce composant n'a pas d'état de bascule intégré.
Utilisé par
- Linear, Grille tarifaire à trois formules avec un plan Pro mis en avant, inversion de couleurs propre sur la carte featured.
- Vercel, Carte mise en avant remplie en accent pour le tier Pro dans une grille 3 colonnes, listes de features avec coches par plan.
- Resend, Layout pricing minimal à 3 cartes avec une carte visuellement élevée, animation d'entrée échelonnée au chargement.
- Lemon Squeezy, Grille de formules tarifaires où le plan recommandé utilise un fond plein qui contraste avec les autres.
FAQ
Comment ajouter un toggle facturation mensuelle/annuelle ?
Ajoute un state `billingCycle` ('monthly' | 'annual') dans un composant parent, passe-le en prop, et dérive le prix affiché depuis ce state dans la carte. L'interface `Tier` peut porter `priceMonthly` et `priceAnnual` pour que le toggle change uniquement quelle valeur s'affiche.
Puis-je utiliser plus ou moins de 3 formules ?
La grille est `md:grid-cols-3` par défaut, 3 formules la remplissent parfaitement. Deux formules laissent une colonne vide ; surcharge avec `md:grid-cols-2`. Quatre formules débordent sur une quatrième colonne en grand écran, ce qui peut paraître déséquilibré. Essaie `max-w-4xl` ou passe à `grid-cols-2 lg:grid-cols-4`.
Pourquoi des styles inline plutôt que des classes Tailwind pour l'état highlighted ?
Les CSS custom properties ne peuvent pas être consommées directement dans la syntaxe de valeurs arbitraires de Tailwind v4 sans config supplémentaire. Les styles inline lisent le token au runtime, ce qui permet au changement de thème de fonctionner instantanément sur les 7 presets sans générer de classes supplémentaires.
L'animation de stagger se rejoue-t-elle quand on remonte ?
Non. `viewport={{ once: true }}` fait que chaque animation se déclenche une seule fois, à la première entrée dans le viewport. Supprime cette option si tu veux que les cartes s'animent à nouveau à chaque fois que la section revient à l'écran.