Volver al blog

Los mejores componentes de página de precios en React para copiar y pegar

Publicado el 15 de marzo de 2026·7 min read

Las páginas de precios son donde las conversiones se ganan o se pierden. Una sección de precios mal estructurada genera dudas; una bien diseñada hace que la elección parezca evidente. Esta guía recoge los patrones exactos que marcan la diferencia: la disposición de 3 planes, el selector mensual/anual, los planes destacados y las señales de confianza, todo implementado en React con Tailwind CSS y TypeScript.

Por qué tres planes

La investigación en psicología sobre la arquitectura de decisiones demuestra de forma consistente que tres opciones funcionan mejor que dos o cuatro. Dos opciones obligan a una elección binaria que se percibe como arriesgada. Cuatro opciones provocan parálisis por análisis. Con tres opciones, el visitante se orienta con facilidad: el plan intermedio actúa como ancla, hace que el plan superior parezca razonable y que el inferior parezca limitado.

Los nombres de los planes también importan. "Starter / Pro / Enterprise" está demasiado visto. Conviene pensar en nombres que reflejen la identidad del cliente: "Solo / Team / Agency", o "Builder / Studio / Scale". El nombre debe hacer que el visitante piense "ese soy yo".

Estructura de datos

Empieza por definir la forma de un plan de precios en TypeScript:

interface Tier {
  name: string;
  monthlyPrice: number;
  yearlyPrice: number;
  currency: "USD" | "EUR";
  description: string;
  features: string[];
  ctaLabel: string;
  ctaUrl: string;
  highlighted: boolean;
  badge?: string; // e.g. "Most popular"
}

Mantener monthlyPrice y yearlyPrice como números separados hace que la lógica del selector sea trivial, sin necesidad de calcular porcentajes en el momento de renderizar.

El selector mensual/anual

El selector es un componente de dos estados con una animación suave tipo pastilla. Usa useState y el motion.div de Framer Motion para el indicador deslizante:

"use client";

import { useState } from "react";
import { motion } from "framer-motion";

const ease: [number, number, number, number] = [0.16, 1, 0.3, 1];

function BillingToggle({ onChange }: { onChange: (yearly: boolean) => void }) {
  const [yearly, setYearly] = useState(false);

  function toggle() {
    const next = !yearly;
    setYearly(next);
    onChange(next);
  }

  return (
    <div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
      <span
        style={{
          fontSize: "0.875rem",
          fontWeight: 500,
          color: !yearly ? "var(--color-foreground)" : "var(--color-foreground-muted)",
        }}
      >
        Monthly
      </span>

      <button
        onClick={toggle}
        style={{
          position: "relative",
          width: "3.5rem",
          height: "1.75rem",
          borderRadius: "var(--radius-full)",
          background: yearly ? "var(--color-accent)" : "var(--color-border)",
          border: "none",
          cursor: "pointer",
          transition: `background var(--duration-normal) var(--ease-out)`,
        }}
        aria-label="Toggle billing period"
        aria-pressed={yearly}
      >
        <motion.div
          animate={{ left: yearly ? "calc(100% - 24px)" : "4px" }}
          transition={{ duration: 0.25, ease }}
          style={{
            position: "absolute",
            top: "4px",
            width: "20px",
            height: "20px",
            borderRadius: "50%",
            background: "var(--color-background)",
          }}
        />
      </button>

      <span
        style={{
          fontSize: "0.875rem",
          fontWeight: 500,
          color: yearly ? "var(--color-foreground)" : "var(--color-foreground-muted)",
        }}
      >
        Annual
        <span
          style={{
            marginLeft: "0.5rem",
            padding: "0.125rem 0.5rem",
            borderRadius: "var(--radius-full)",
            background: "var(--color-accent)",
            color: "var(--color-background)",
            fontSize: "0.75rem",
            fontWeight: 600,
            opacity: yearly ? 1 : 0.5,
            transition: `opacity var(--duration-normal) var(--ease-out)`,
          }}
        >
          Save 20%
        </span>
      </span>
    </div>
  );
}

El atributo aria-pressed hace que el selector sea accesible para los lectores de pantalla. El aria-label aporta contexto cuando el botón no tiene una etiqueta de texto visible propia.

Plan destacado

El plan "Most Popular" tiene que destacar visualmente frente a los demás. La técnica más eficaz invierte el esquema de color: la tarjeta destacada usa var(--color-foreground) como fondo y var(--color-background) para el texto, lo que crea una jerarquía visual natural sin añadir colores extra:

function PricingCard({ tier, yearly }: { tier: Tier; yearly: boolean }) {
  const price = yearly ? tier.yearlyPrice : tier.monthlyPrice;

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        borderRadius: "var(--radius-xl)",
        padding: "2rem",
        background: tier.highlighted
          ? "var(--color-foreground)"
          : "var(--color-background-card)",
        border: tier.highlighted
          ? "none"
          : "1px solid var(--color-border)",
        position: "relative",
      }}
    >
      {tier.badge && tier.highlighted && (
        <span
          style={{
            position: "absolute",
            top: "-0.75rem",
            left: "50%",
            transform: "translateX(-50%)",
            padding: "0.25rem 1rem",
            borderRadius: "var(--radius-full)",
            background: "var(--color-accent)",
            color: "var(--color-foreground)",
            fontSize: "0.75rem",
            fontWeight: 700,
            letterSpacing: "0.05em",
            textTransform: "uppercase",
            whiteSpace: "nowrap",
          }}
        >
          {tier.badge}
        </span>
      )}
      {/* Price, features, CTA */}
    </div>
  );
}

Colocar la insignia con top: -0.75rem y centrarla horizontalmente hace que "flote" por encima de la tarjeta, lo que atrae la mirada de forma natural.

Animación del precio al cambiar de periodo

Cuando el usuario cambia el periodo de facturación, anima la transición del precio para que se sienta fluida en lugar de brusca:

import { AnimatePresence, motion } from "framer-motion";

// Inside PricingCard
<AnimatePresence mode="wait">
  <motion.span
    key={price}  // Changing the key triggers exit + enter animation
    initial={{ opacity: 0, y: -10 }}
    animate={{ opacity: 1, y: 0 }}
    exit={{ opacity: 0, y: 10 }}
    transition={{ duration: 0.2 }}
    style={{
      fontSize: "2.5rem",
      fontWeight: 700,
      color: tier.highlighted
        ? "var(--color-background)"
        : "var(--color-foreground)",
    }}
  >
    ${price}
  </motion.span>
</AnimatePresence>

El truco de key={price} es esencial: React usa la key para distinguir entre elementos, así que al cambiarla le indicas a AnimatePresence que haga salir el precio antiguo y entrar el nuevo.

Lista de características

Mantén las descripciones de las características cortas y concretas. "Unlimited projects" funciona mejor que "No project limit". Usa un icono de check de lucide-react:

import { Check } from "lucide-react";

<ul style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
  {tier.features.map((feature) => (
    <li
      key={feature}
      style={{ display: "flex", alignItems: "center", gap: "0.75rem", fontSize: "0.875rem" }}
    >
      <Check
        size={16}
        style={{
          flexShrink: 0,
          color: tier.highlighted
            ? "var(--color-background)"
            : "var(--color-accent)",
        }}
      />
      <span
        style={{
          color: tier.highlighted
            ? "var(--color-background)"
            : "var(--color-foreground)",
          opacity: tier.highlighted ? 0.85 : 1,
        }}
      >
        {feature}
      </span>
    </li>
  ))}
</ul>

Señales de confianza debajo de la rejilla

La rejilla por sí sola no basta. Añade una fila de señales de confianza justo debajo de las tarjetas de precios. Son las que convierten a los indecisos:

const trustSignals = [
  "No credit card required",
  "14-day free trial",
  "Cancel anytime",
  "SOC 2 Type II certified",
];

<div
  style={{
    display: "flex",
    flexWrap: "wrap",
    justifyContent: "center",
    gap: "1.5rem",
    marginTop: "2.5rem",
  }}
>
  {trustSignals.map((signal) => (
    <span
      key={signal}
      style={{
        display: "flex",
        alignItems: "center",
        gap: "0.5rem",
        fontSize: "0.8125rem",
        color: "var(--color-foreground-muted)",
      }}
    >
      <Check size={14} style={{ color: "var(--color-accent)" }} />
      {signal}
    </span>
  ))}
</div>

Aparición escalonada de las tarjetas al hacer scroll

Usa whileInView con un retardo basado en el índice de la tarjeta para que las tarjetas aparezcan en cascada a medida que el usuario hace scroll:

<motion.div
  initial={{ opacity: 0, y: 30 }}
  whileInView={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.5, ease, delay: index * 0.1 }}
  viewport={{ once: true }}
>
  <PricingCard tier={tier} yearly={yearly} />
</motion.div>

El viewport={{ once: true }} evita que la animación se vuelva a disparar cada vez que el usuario pasa por delante de la sección.

Componentes de precios listos para usar

Si quieres saltarte el código repetitivo e ir directo a la personalización, el catálogo de precios de Incubator tiene más de 20 variantes de secciones de precios: disposiciones con selector, comparativas tipo matriz de características, sliders por uso, muros freemium, tarjetas con foco y muchas más, todas construidas con los patrones anteriores y listas para integrar en tu proyecto.

Relacionado en incubator

VA

Victor Aubague

Desarrollador y creador de Incubator

Desarrollador full-stack especializado en React, Next.js y TypeScript. Creé Incubator para ayudar a los desarrolladores a lanzar interfaces bonitas más rápido, todos los componentes provienen de proyectos reales de clientes y de código en producción.

LinkedIn

Related articles

Los mejores componentes de página de precios en React para