Ir al contenido
Evaluar mi proyecto
Componentes · PreciosDisponible

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.

$0/ mes
  • 1 proyecto
  • Hasta 3 personas
  • Soporte por correo
Empezar gratis
Más popular

Equipo

Para equipos que ya están en marcha.

$29/ mes
  • Proyectos ilimitados
  • Hasta 20 personas
  • Automatizaciones
  • Soporte prioritario
Elegir Equipo

Empresa

Para organizaciones con más control.

$79/ mes
  • Todo lo de Equipo
  • Personas ilimitadas
  • Inicio de sesión único
  • Acompañamiento dedicado
Hablar con ventas

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

pricing-tiers.tsx126 líneas
Descargar
"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.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/precios-tres-planes

Ejemplo 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.

ejemplo.tsx5 líneas
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.

PropiedadTipoDescripción
title?stringOpcional.
description?stringOpcional.
plans?PricingPlan[]Opcional.
currency?stringOpcional.
featuredLabel?stringTexto de la etiqueta del plan destacado.
className?stringOpcional.
style?CSSPropertiesOpcional.

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".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React · Next.js (quita la directiva use client fuera de Next.js) · Sin Tailwind

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.