Retour au catalogue

Pricing Cards

Grille de 3 cartes de prix avec highlight sur la formule recommandee. Clean et lisible.

pricingsimple Both Responsive a11y
minimalcorporatesaasagencyuniversalgrid
Theme

How to build a 3-column pricing cards section in React

A React pricing cards section renders 2-4 tier objects in a CSS grid and uses a `highlighted` boolean to invert the accent-colored card, swapping background and foreground tokens so the recommended plan stands out without any extra markup. Framer Motion animates each card in on scroll with a staggered delay.

  • Stack: React 18 + Framer Motion 11 + Tailwind v4 + lucide-react, ~195 lines total.
  • Animation: whileInView fade-up with stagger (i * 0.1s delay) and a custom spring ease [0.16, 1, 0.3, 1].
  • Theming: 100% CSS custom properties, no hardcoded colors, works across all 7 presets.
  • Accessible: semantic h3 per tier, Check icon is decorative (no aria-label needed), button elements for CTAs.
  • Fully responsive: single column on mobile, 3-column grid from md breakpoint.

Pricing Cards is a responsive 3-tier pricing section where one card flips to an accent-filled background to signal the recommended plan. The header slides up on scroll, then each card follows with a staggered fade-up, subtle enough to feel polished, fast enough not to block conversion. The whole thing runs on CSS tokens, so it adapts to any color preset without touching a line of component code.

Anatomy

The component has two regions. At the top, a centered header block contains an optional uppercase badge (accent color), an h2 title, and an optional subtitle paragraph. Below it, a `grid md:grid-cols-3` lays out the tier cards. Each card is a `flex flex-col rounded-xl p-8` div, name, description, price display, a flex-1 features list with Check icons, then a full-width CTA button pinned to the bottom. When `highlighted` is true, the card swaps its background to `--color-accent`, all text tokens to `--color-background`, and a small pill badge reading 'Populaire' appears above the top edge via absolute positioning.

How it works

Both the header and the cards use Framer Motion's `whileInView` with `viewport={{ once: true }}` so the animation fires once when the section scrolls into view. The header uses a single `{ opacity: 0, y: 20 }` to `{ opacity: 1, y: 0 }` transition at 0.6s. Each card runs the same fade-up but with `delay: i * 0.1` computed from the array index, creating a left-to-right cascade. Both share the same custom cubic bezier `[0.16, 1, 0.3, 1]`, a fast-out-slow-in curve that gives the entrance weight without dragging.

How to build it in React

  1. Define the Tier interface and props

    Create a `Tier` interface with `name`, `price`, `currency`, `period`, `description`, `features`, `highlighted`, and `ctaLabel`. The component accepts `badge`, `title`, `subtitle`, and `tiers` as optional props with sensible defaults. Keeping the data external means the same component serves every product.

    interface Tier {
      name: string;
      price: number;
      currency: string;
      period: string;
      description: string;
      features: string[];
      highlighted: boolean;
      ctaLabel: string;
    }
  2. Animate the header on scroll

    Wrap the header block in a `motion.div` with `initial={{ opacity: 0, y: 20 }}`, `whileInView={{ opacity: 1, y: 0 }}`, and `viewport={{ once: true }}`. Use the custom spring ease so it snaps in cleanly. The header fires first, before any card, giving the section a two-beat entrance.

    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 }}
    >
  3. Stagger the cards by index

    Map over `tiers` and wrap each in a `motion.div` with `delay: i * 0.1`. The first card enters at 0ms, the second at 100ms, the third at 200ms. Keep the duration at 0.5s, shorter than the header so they feel snappier.

    {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 }}
      >
  4. Invert the highlighted card with CSS tokens

    Use the `highlighted` boolean to swap background and foreground inline styles. The card background becomes `var(--color-accent)`, all text switches to `var(--color-background)`, and the Check icons flip to `var(--color-background)` as well. No extra CSS class needed. The 'Populaire' badge is absolutely positioned with a negative top offset so it overlaps the card border.

    style={{
      background: tier.highlighted
        ? "var(--color-accent)"
        : "var(--color-background-card)",
      border: tier.highlighted
        ? "none"
        : "1px solid var(--color-border)",
    }}

When to use it

Use it on SaaS, agency, or product landing pages where you need a clean conversion moment after the features section. The 3-column layout works well for Free/Pro/Enterprise tiers. Skip it if you have more than 4 plans, beyond that, a comparison table reads better. Also skip it if you need a toggle between monthly/annual billing; this component has no built-in toggle state.

Used by

  • Linear, Three-tier pricing grid with a highlighted Pro plan, clean token-based color inversion on the featured card.
  • Vercel, Accent-filled highlighted card for the Pro tier in a 3-column grid, feature lists with checkmarks per plan.
  • Resend, Minimal 3-card pricing layout with one card visually elevated, staggered entrance animation on load.
  • Lemon Squeezy, Grid of pricing tiers where the recommended plan uses a filled background contrasting with the others.

FAQ

How do I add a monthly/annual billing toggle?

Add a `billingCycle` state ('monthly' | 'annual') in a parent component, pass it as a prop, and derive the displayed price from it inside the card. The `Tier` interface can hold both `priceMonthly` and `priceAnnual` so the toggle only changes which value renders.

Can I use more or fewer than 3 tiers?

The grid is `md:grid-cols-3` by default, so 3 tiers fill it perfectly. Two tiers leave an empty column; override with `md:grid-cols-2`. Four tiers overflow to a fourth column on wide screens, which can look unbalanced, consider a `max-w-4xl` container or switching to `grid-cols-2 lg:grid-cols-4`.

Why use inline styles instead of Tailwind classes for the highlighted state?

CSS custom properties can't be consumed directly inside arbitrary Tailwind value syntax in v4 without additional config. Inline styles read the token at runtime, which means theme switching works instantly across all 7 presets without any extra class generation.

Does the stagger animation replay when scrolling back up?

No. `viewport={{ once: true }}` makes each animation fire a single time, the first time the element enters the viewport. Remove that option if you want the cards to re-animate every time the section comes back into view.

"use client";

import { motion } from "framer-motion";
import { Check } from "lucide-react";

interface Tier {
  name: string;
  price: number;
  currency: string;
  period: string;
  description: string;
  features: string[];
  highlighted: boolean;
  ctaLabel: string;
}

interface PricingCardsProps {
  badge?: string;
  title?: string;
  subtitle?: string;
  tiers?: Tier[];
}

Code complet réservé à Pro

Code source intégral, export multi-framework et playground.

Passer en Pro, 9,99€/mois

Reviews

React Pricing Cards Grid with Highlighted Tier, Tutorial