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

Cómo crear una sección de tarjetas de precios de 3 columnas en React

Una sección de tarjetas de precios en React renderiza de 2 a 4 objetos tier en un grid CSS y usa un booleano `highlighted` para invertir la tarjeta con color de acento, intercambiando los tokens de fondo y texto para que el plan recomendado destaque sin markup adicional. Framer Motion anima la entrada de cada tarjeta al hacer scroll con un retardo escalonado.

  • Stack: React 18 + Framer Motion 11 + Tailwind v4 + lucide-react, ~195 líneas en total.
  • Animación: fade-up whileInView con stagger (retardo i * 0.1s) y un ease spring personalizado [0.16, 1, 0.3, 1].
  • Theming: 100% CSS custom properties, sin colores hardcodeados, funciona en los 7 presets.
  • Accesible: h3 semántico por plan, el icono Check es decorativo (no necesita aria-label), elementos button para los CTAs.
  • Totalmente responsive: una sola columna en móvil, grid de 3 columnas a partir del breakpoint md.

Pricing Cards es una sección de precios responsive de 3 planes donde una tarjeta cambia a un fondo relleno de acento para señalar el plan recomendado. El encabezado sube al hacer scroll, luego cada tarjeta lo sigue con un fade-up escalonado. Lo bastante sutil para verse pulido, lo bastante rápido para no bloquear la conversión. Todo funciona con CSS tokens, así que se adapta a cualquier preset de colores sin tocar una línea del código del componente.

Anatomía

El componente tiene dos zonas. Arriba, un bloque de encabezado centrado contiene un badge en mayúsculas opcional (color de acento), un título h2 y un párrafo de subtítulo opcional. Debajo, un `grid md:grid-cols-3` distribuye las tarjetas de plan. Cada tarjeta es un div `flex flex-col rounded-xl p-8`: nombre, descripción, visualización del precio, una lista de features flex-1 con iconos Check, y luego un botón CTA de ancho completo anclado abajo. Cuando `highlighted` es true, la tarjeta cambia su fondo a `--color-accent`, todos los tokens de texto a `--color-background`, y un pequeño badge de tipo pill con el texto 'Populaire' aparece sobre el borde superior mediante posicionamiento absoluto.

Cómo funciona

Tanto el encabezado como las tarjetas usan el `whileInView` de Framer Motion con `viewport={{ once: true }}` para que la animación se dispare una sola vez cuando la sección entra en el viewport al hacer scroll. El encabezado usa una única transición de `{ opacity: 0, y: 20 }` a `{ opacity: 1, y: 0 }` en 0.6s. Cada tarjeta hace el mismo fade-up pero con `delay: i * 0.1` calculado a partir del índice del array, creando una cascada de izquierda a derecha. Ambos comparten el mismo cubic bezier personalizado `[0.16, 1, 0.3, 1]`, una curva fast-out-slow-in que da peso a la entrada sin ralentizarla.

Cómo crearlo en React

  1. Definir la interfaz Tier y las props

    Crea una interfaz `Tier` con `name`, `price`, `currency`, `period`, `description`, `features`, `highlighted` y `ctaLabel`. El componente acepta `badge`, `title`, `subtitle` y `tiers` como props opcionales con valores por defecto razonables. Mantener los datos externos permite usar el mismo componente para cualquier producto.

    interface Tier {
      name: string;
      price: number;
      currency: string;
      period: string;
      description: string;
      features: string[];
      highlighted: boolean;
      ctaLabel: string;
    }
  2. Animar el encabezado al hacer scroll

    Envuelve el bloque de encabezado en un `motion.div` con `initial={{ opacity: 0, y: 20 }}`, `whileInView={{ opacity: 1, y: 0 }}` y `viewport={{ once: true }}`. Usa el ease spring personalizado para que entre de forma limpia. El encabezado se dispara primero, antes que cualquier tarjeta, dando a la sección una entrada en dos tiempos.

    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. Escalonar las tarjetas por índice

    Recorre `tiers` y envuelve cada tarjeta en un `motion.div` con `delay: i * 0.1`. La primera tarjeta entra a 0ms, la segunda a 100ms, la tercera a 200ms. Mantén la duración en 0.5s, más corta que el encabezado para que se sientan más ágiles.

    {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. Invertir la tarjeta destacada con CSS tokens

    Usa el booleano `highlighted` para intercambiar los estilos inline de fondo y texto. El fondo de la tarjeta pasa a `var(--color-accent)`, todo el texto cambia a `var(--color-background)`, y los iconos Check también pasan a `var(--color-background)`. No hace falta ninguna clase CSS adicional. El badge 'Populaire' está posicionado en absoluto con un offset top negativo para que solape el borde de la tarjeta.

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

Cuándo usarlo

Úsalo en landing pages de SaaS, agencias o productos donde necesites un momento de conversión limpio después de la sección de features. El layout de 3 columnas funciona bien para planes Gratis/Pro/Empresa. Evítalo si tienes más de 4 planes, a partir de ahí una tabla comparativa se lee mejor. Evítalo también si necesitas un toggle entre facturación mensual/anual; este componente no tiene estado de toggle integrado.

Usado por

  • Linear, Grid de precios de tres planes con un plan Pro destacado, inversión de colores limpia basada en tokens en la tarjeta destacada.
  • Vercel, Tarjeta destacada rellena de acento para el plan Pro en un grid de 3 columnas, listas de features con marcas de verificación por plan.
  • Resend, Layout de precios minimalista de 3 tarjetas con una tarjeta visualmente elevada, animación de entrada escalonada al cargar.
  • Lemon Squeezy, Grid de planes de precios donde el plan recomendado usa un fondo relleno que contrasta con los demás.

FAQ

¿Cómo añado un toggle de facturación mensual/anual?

Añade un state `billingCycle` ('monthly' | 'annual') en un componente padre, pásalo como prop, y deriva el precio mostrado a partir de él dentro de la tarjeta. La interfaz `Tier` puede contener tanto `priceMonthly` como `priceAnnual` para que el toggle solo cambie qué valor se renderiza.

¿Puedo usar más o menos de 3 planes?

El grid es `md:grid-cols-3` por defecto, así que 3 planes lo llenan perfectamente. Dos planes dejan una columna vacía; sobrescríbelo con `md:grid-cols-2`. Cuatro planes desbordan a una cuarta columna en pantallas anchas, lo que puede verse desequilibrado. Considera un contenedor `max-w-4xl` o pasar a `grid-cols-2 lg:grid-cols-4`.

¿Por qué usar estilos inline en lugar de clases Tailwind para el estado highlighted?

Las CSS custom properties no se pueden consumir directamente dentro de la sintaxis de valores arbitrarios de Tailwind v4 sin config adicional. Los estilos inline leen el token en runtime, lo que significa que el cambio de tema funciona al instante en los 7 presets sin generar ninguna clase adicional.

¿La animación de stagger se vuelve a reproducir al volver a subir?

No. `viewport={{ once: true }}` hace que cada animación se dispare una sola vez, la primera vez que el elemento entra en el viewport. Quita esa opción si quieres que las tarjetas se vuelvan a animar cada vez que la sección regresa a la vista.

"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

Reseñas

Grid de tarjetas de precios en React con plan destacado,