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
- Componentes de sección de precios: tablas de precios, selectores y planes para copiar y pegar.
- Componentes de sección CTA: secciones de llamada a la acción centradas en la conversión.
- Componentes de sección de estadísticas: contadores animados y cifras de prueba social.
- Incubator vs Tailwind UI: mira cómo se comparan las librerías para páginas de precios.
- Catálogo completo de componentes: explora las 449 variantes de sección por tipo.