Precios de tres planes
Tres planes con selector mensual/anual y el plan destacado invertido.
Un plan para cada etapa
Empieza gratis y cambia de plan cuando lo necesites. Sin letra pequeña.
Inicio
Para probar la idea sin compromiso.
- 1 proyecto
- Hasta 3 personas
- Soporte por correo
Equipo
Para equipos que ya están en marcha.
- Proyectos ilimitados
- Hasta 20 personas
- Automatizaciones
- Soporte prioritario
Empresa
Para organizaciones con más control.
- Todo lo de Equipo
- Personas ilimitadas
- Inicio de sesión único
- Acompañamiento dedicado
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
/**
* Sección de precios con tres planes y selector mensual/anual: el plan destacado va invertido y el precio cambia con una animación corta.
* Pasa tus planes; los valores por defecto son solo de ejemplo.
*
* <PricingTiers
* plans={[
* { name: "Inicio", description: "Para probar", monthly: 0, yearly: 0, features: ["1 proyecto"], cta: { label: "Empezar", href: "/registro" } },
* { name: "Equipo", description: "Para crecer", monthly: 29, yearly: 24, featured: true, features: ["Proyectos ilimitados"], cta: { label: "Elegir Equipo", href: "/registro?plan=equipo" } },
* ]}
* />
*/
import { useState, type CSSProperties } from "react";
export type PricingPlan = {
name: string;
description: string;
/** Precio por mes facturado mensualmente. */
monthly: number;
/** Precio por mes facturado cada año. */
yearly: number;
features: string[];
cta: { label: string; href: string };
featured?: boolean;
};
export type PricingTiersProps = {
title?: string;
description?: string;
plans?: PricingPlan[];
currency?: string;
/** Texto de la etiqueta del plan destacado. */
featuredLabel?: string;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcpt-root { --gcpt-accent: #6d4aff; box-sizing: border-box; padding: clamp(36px, 6vw, 88px) clamp(16px, 4vw, 56px); background: #f3f5f9; color: #141824; font-family: inherit; }
.gcpt-head { max-width: 640px; margin: 0 auto; text-align: center; }
.gcpt-head h2 { margin: 0; font-size: clamp(32px, 4.6vw, 56px); font-weight: 700; line-height: 1.04; letter-spacing: -.04em; text-wrap: balance; }
.gcpt-head p { margin: 14px 0 0; color: #566075; font-size: 17px; line-height: 1.55; }
.gcpt-switch { position: relative; display: inline-grid; grid-template-columns: 1fr 1fr; margin-top: 28px; padding: 4px; border-radius: 999px; background: #e1e5ee; isolation: isolate; }
.gcpt-switch::before { content: ""; position: absolute; top: 4px; bottom: 4px; left: 4px; z-index: -1; width: calc(50% - 4px); border-radius: 999px; background: #141824; transition: transform .45s cubic-bezier(.34, 1.4, .5, 1); }
.gcpt-switch[data-yearly="true"]::before { transform: translateX(100%); }
.gcpt-switch button { min-height: 40px; padding: 0 22px; border: 0; border-radius: 999px; background: none; color: #3a4357; font-family: inherit; font-size: 15px; font-weight: 600; cursor: pointer; transition: color .25s; white-space: nowrap; }
.gcpt-switch button[aria-pressed="true"] { color: #fff; }
.gcpt-switch button:focus-visible { outline: 2px solid var(--gcpt-accent); outline-offset: 2px; }
.gcpt-switch small { margin-left: 6px; padding: 2px 7px; border-radius: 999px; background: #d6f8e3; color: #11643a; font-size: 11px; font-weight: 700; }
.gcpt-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; max-width: 1060px; margin: 44px auto 0; align-items: stretch; }
.gcpt-card { position: relative; display: flex; flex-direction: column; padding: 30px; border-radius: 24px; background: #fff; box-shadow: 0 1px 2px rgba(20, 24, 36, .05), 0 18px 40px -26px rgba(20, 24, 36, .3); }
.gcpt-card[data-featured="true"] { background: #141824; color: #fff; box-shadow: 0 30px 60px -24px rgba(20, 24, 36, .65); }
.gcpt-tag { position: absolute; top: -13px; left: 30px; padding: 5px 13px; border-radius: 999px; background: var(--gcpt-accent); color: #fff; font-size: 12px; font-weight: 700; }
.gcpt-card h3 { margin: 0; font-size: 20px; font-weight: 700; letter-spacing: -.02em; }
.gcpt-card > p { margin: 6px 0 0; color: #6a7388; font-size: 15px; line-height: 1.5; }
.gcpt-card[data-featured="true"] > p { color: #a5adc4; }
.gcpt-price { display: flex; align-items: baseline; gap: 6px; margin: 24px 0 4px; }
.gcpt-amount { display: inline-block; font-size: 52px; font-weight: 700; letter-spacing: -.05em; line-height: 1; font-variant-numeric: tabular-nums; animation: gcpt-swap .45s cubic-bezier(.16, 1, .3, 1); }
.gcpt-price span:last-child { color: #7a8398; font-size: 15px; }
.gcpt-card[data-featured="true"] .gcpt-price span:last-child { color: #a5adc4; }
.gcpt-card ul { display: grid; gap: 12px; margin: 26px 0 30px; padding: 0; list-style: none; font-size: 15px; line-height: 1.4; }
.gcpt-card li { display: flex; gap: 11px; align-items: flex-start; }
.gcpt-card li svg { flex: none; width: 18px; height: 18px; margin-top: 1px; color: var(--gcpt-accent); }
.gcpt-card[data-featured="true"] li svg { color: #b9a6ff; }
.gcpt-cta { display: inline-flex; align-items: center; justify-content: center; min-height: 50px; margin-top: auto; border-radius: 14px; background: #141824; color: #fff; font-size: 16px; font-weight: 600; text-decoration: none; transition: background-color .25s, transform .3s cubic-bezier(.16, 1, .3, 1); }
.gcpt-cta:hover { background: var(--gcpt-accent); transform: translateY(-2px); }
.gcpt-card[data-featured="true"] .gcpt-cta { background: #fff; color: #141824; }
.gcpt-card[data-featured="true"] .gcpt-cta:hover { background: #e6dfff; }
.gcpt-cta:focus-visible { outline: 2px solid var(--gcpt-accent); outline-offset: 3px; }
@keyframes gcpt-swap { from { opacity: 0; transform: translateY(10px); filter: blur(4px); } }
@media (prefers-reduced-motion: reduce) { .gcpt-switch::before, .gcpt-cta { transition: none; } .gcpt-amount { animation: none; } }
`;
const DEFAULT_PLANS: PricingPlan[] = [
{ name: "Inicio", description: "Para probar la idea sin compromiso.", monthly: 0, yearly: 0, features: ["1 proyecto", "Hasta 3 personas", "Soporte por correo"], cta: { label: "Empezar gratis", href: "#registro" } },
{ name: "Equipo", description: "Para equipos que ya están en marcha.", monthly: 29, yearly: 24, featured: true, features: ["Proyectos ilimitados", "Hasta 20 personas", "Automatizaciones", "Soporte prioritario"], cta: { label: "Elegir Equipo", href: "#registro" } },
{ name: "Empresa", description: "Para organizaciones con más control.", monthly: 79, yearly: 66, features: ["Todo lo de Equipo", "Personas ilimitadas", "Inicio de sesión único", "Acompañamiento dedicado"], cta: { label: "Hablar con ventas", href: "#contacto" } },
];
const check = <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5" /></svg>;
export function PricingTiers({
title = "Un plan para cada etapa",
description = "Empieza gratis y cambia de plan cuando lo necesites. Sin letra pequeña.",
plans = DEFAULT_PLANS,
currency = "$",
featuredLabel = "Más popular",
className,
style,
}: PricingTiersProps) {
const [yearly, setYearly] = useState(false);
return (
<>
<style>{CSS}</style>
<section className={["gcpt-root", className].filter(Boolean).join(" ")} style={style}>
<div className="gcpt-head">
<h2>{title}</h2>
<p>{description}</p>
<div className="gcpt-switch" data-yearly={yearly} role="group" aria-label="Periodo de facturación">
<button type="button" aria-pressed={!yearly} onClick={() => setYearly(false)}>Mensual</button>
<button type="button" aria-pressed={yearly} onClick={() => setYearly(true)}>Anual<small>Ahorra</small></button>
</div>
</div>
<div className="gcpt-grid">
{plans.map((plan) => (
<article className="gcpt-card" key={plan.name} data-featured={plan.featured ? "true" : undefined}>
{plan.featured ? <span className="gcpt-tag">{featuredLabel}</span> : null}
<h3>{plan.name}</h3>
<p>{plan.description}</p>
<div className="gcpt-price">
<span className="gcpt-amount" key={`${plan.name}-${yearly}`}>{currency}{yearly ? plan.yearly : plan.monthly}</span>
<span>/ mes{yearly && plan.yearly > 0 ? ", facturado al año" : ""}</span>
</div>
<ul>{plan.features.map((feature) => <li key={feature}>{check}{feature}</li>)}</ul>
<a className="gcpt-cta" href={plan.cta.href}>{plan.cta.label}</a>
</article>
))}
</div>
</section>
</>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/precios-tres-planesEjemplo de uso de Precios de tres planes
Después de copiar pricing-tiers.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { PricingTiers } from "./pricing-tiers";
export default function Ejemplo() {
return <PricingTiers title="Ejemplo" />;
}Propiedades de PricingTiers
PricingTiers acepta 7 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
title? | string | Opcional. |
description? | string | Opcional. |
plans? | PricingPlan[] | Opcional. |
currency? | string | Opcional. |
featuredLabel? | string | Texto de la etiqueta del plan destacado. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 126 líneas (8 KB) y define 1 animación con @keyframes. Esto es lo que hace Precios de tres planes por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
- Declara roles ARIA para lectores de pantalla: group.
- Es un componente de cliente de Next.js: incluye la directiva "use client".
Acerca del producto
La sección de precios clásica: tres planes con selector mensual o anual (con una pastilla que se desliza), el plan destacado en oscuro con su etiqueta, el precio que cambia con una animación corta y una lista de lo incluido. Los datos por defecto son solo de ejemplo.
Incluye
Una sección completa en un solo archivo React + TypeScript, con los estilos dentro. No usa Tailwind ni bibliotecas externas.
- Selector mensual/anual accesible (aria-pressed)
- Planes, precios y moneda configurables
- Plan destacado con etiqueta
- Cuadrícula que se adapta al ancho
Cómo usarlo
Pasa plans con name, description, monthly, yearly, features y cta; marca el destacado con featured.