Ir al contenido
Evaluar mi proyecto
Componentes · PreciosDisponible

Configurador de plan modular

Plan modular con casillas reales, desglose de extras y suma mensual; devuelve la selección y el total mediante callback.

Tu plan.
A tu medida.

Parte de lo esencial y añade lo que necesitas.

Plan baseUSD 19
Total mensualUSD 34USD · precios de muestra
  • Plan base incluido
  • Espacio de equipo

Puedes cambiar la selección antes de continuar.

Código completo

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

precios-configurador.tsx133 líneas
Descargar
"use client";

import { useState, type CSSProperties } from "react";

/** Configurador de plan modular. React + TypeScript, sin dependencias. Personaliza con variables --gczo-*. */
export type PlanModule = { id: string; label: string; price: number };
export type PreciosConfiguradorProps = {
  basePrice?: number;
  modules?: PlanModule[];
  currency?: string;
  onChoose?: (ids: string[], total: number) => void;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gczo-root { box-sizing: border-box; min-width: 0; max-width: 100%; color: var(--gczo-fg, #202421); font: 400 15px/1.5 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.gczo-root *, .gczo-root *::before, .gczo-root *::after { box-sizing: border-box; }
.gczo-root button, .gczo-root input, .gczo-root textarea, .gczo-root select { font: inherit; }
.gczo-root button, .gczo-root a { -webkit-tap-highlight-color: transparent; }
.gczo-root :focus-visible { outline: 2px solid var(--gczo-accent, #25724b); outline-offset: 4px; }
.gczo-root ::selection { background: var(--gczo-accent, #25724b); color: #fff; }
.gczo-root button:disabled { opacity: .5; cursor: not-allowed; }
.gczo-root svg { flex-shrink: 0; }
@media (prefers-reduced-motion: reduce) { .gczo-root *, .gczo-root *::before, .gczo-root *::after { animation: none !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } }
.gczo-root { display:grid; grid-template-columns:1.2fr 1fr; width:100%; background:var(--gczo-bg,#ecf0f4); color:#263343; padding:44px clamp(24px,5vw,60px); gap:40px; }
.gczo-options h2 { font-size:42px; font-weight:600; line-height:1.05; letter-spacing:-.03em; margin:0 0 20px; }
.gczo-options>p { font-size:14px; color:#526174; margin:0 0 24px; }
.gczo-base,.gczo-options label { display:flex; align-items:center; gap:12px; padding:17px 0; border-bottom:1px solid #c6cfdb; font-size:13px; }
.gczo-base strong,.gczo-price { margin-left:auto; font-variant-numeric:tabular-nums; }
.gczo-options input { width:20px; height:20px; accent-color:var(--gczo-accent,#3c587b); cursor:pointer; }
.gczo-options label { min-height:54px; cursor:pointer; }
.gczo-summary { padding:28px; border-radius:12px; background:#fff; display:flex; flex-direction:column; align-items:flex-start; }
.gczo-summary>span { font-size:12px; color:#526174; }
.gczo-summary>strong { font-size:52px; line-height:1.2; letter-spacing:-.03em; font-weight:600; margin-top:12px; font-variant-numeric:tabular-nums; }
.gczo-summary small { font-size:11px; color:#526174; }
.gczo-summary ul { font-size:13px; padding-left:18px; margin:28px 0; }
.gczo-summary li { margin:8px 0; }
.gczo-summary button { width:100%; margin-top:auto; min-height:48px; border:0; border-radius:5px; background:#3c587b; color:#fff; padding:12px; cursor:pointer; }
.gczo-summary button:hover { background:#293f5b; }
.gczo-summary p { font-size:11px; color:#526174; margin:16px 0 0; }
@media(max-width:620px) { .gczo-root { grid-template-columns:1fr; gap:28px; } }`;

export function PreciosConfigurador({
  basePrice = 19,
  modules = [
    { id: "analytics", label: "Analítica avanzada", price: 9 },
    { id: "team", label: "Espacio de equipo", price: 15 },
    { id: "support", label: "Soporte prioritario", price: 12 },
  ],
  currency = "USD",
  onChoose,
  className = "",
  style,
}: PreciosConfiguradorProps) {
  const [selected, setSelected] = useState<string[]>(["team"]);
  const [chosen, setChosen] = useState(false);
  const total =
    basePrice +
    modules
      .filter((m) => selected.includes(m.id))
      .reduce((sum, m) => sum + m.price, 0);
  const fmt = new Intl.NumberFormat("es-MX", {
    style: "currency",
    currency,
    maximumFractionDigits: 0,
  });
  return (
    <section className={`gczo-root ${className}`} style={style}>
      <style>{CSS}</style>
      <div className="gczo-options">
        <h2>
          Tu plan.
          <br />A tu medida.
        </h2>
        <p>Parte de lo esencial y añade lo que necesitas.</p>
        <div className="gczo-base">
          <span>Plan base</span>
          <strong>{fmt.format(basePrice)}</strong>
        </div>
        {modules.map((module) => (
          <label key={module.id}>
            <span>{module.label}</span>
            <span className="gczo-price">+{fmt.format(module.price)}</span>
            <input
              type="checkbox"
              checked={selected.includes(module.id)}
              onChange={(e) => {
                setSelected(
                  e.target.checked
                    ? [...selected, module.id]
                    : selected.filter((id) => id !== module.id),
                );
                setChosen(false);
              }}
            />
          </label>
        ))}
      </div>
      <div className="gczo-summary">
        <span>Total mensual</span>
        <strong>{fmt.format(total)}</strong>
        <small>{currency} · precios de muestra</small>
        <ul>
          <li>Plan base incluido</li>
          {modules
            .filter((m) => selected.includes(m.id))
            .map((m) => (
              <li key={m.id}>{m.label}</li>
            ))}
        </ul>
        <button
          type="button"
          onClick={() => {
            onChoose?.(
              selected.filter((id) => modules.some((m) => m.id === id)),
              total,
            );
            setChosen(true);
          }}
        >
          Elegir configuración
        </button>
        <p role="status">
          {chosen
            ? "Configuración seleccionada. No se realizó ningún cobro."
            : "Puedes cambiar la selección antes de continuar."}
        </p>
      </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-configurador

Ejemplo de uso de Configurador de plan modular

Después de copiar precios-configurador.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { PreciosConfigurador } from "./precios-configurador";

export default function Ejemplo() {
  return <PreciosConfigurador currency="Ejemplo" />;
}

Propiedades de PreciosConfigurador

PreciosConfigurador acepta 6 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

PropiedadTipoDescripción
basePrice?numberOpcional.
modules?PlanModule[]Opcional.
currency?stringOpcional.
onChoose?(ids: string[], total: number) => voidOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Personaliza Configurador de plan modular con variables CSS

Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.

VariableValor por defecto
--gczo-fg#202421
--gczo-accent#25724b
--gczo-bg#ecf0f4

Accesibilidad y rendimiento

El archivo tiene 133 líneas (6 KB) y no necesita @keyframes. Esto es lo que hace Configurador de plan modular 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: status.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)