Precios por uso con deslizador
Un deslizador de volumen calcula el precio mensual al instante.
Paga solo por lo que usas
Mueve el deslizador y mira cuánto pagarías cada mes. Sin mínimos ni contratos.
- Todas las funciones incluidas
- Panel de uso en tiempo real
- Alertas antes de pasarte
- Soporte por correo
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
/**
* Precios por uso: un deslizador de volumen mensual calcula el precio al instante, con una pista que se llena y un precio que cambia con una animación corta.
* El deslizador es un <input type="range"> nativo, así que funciona con teclado y lectores de pantalla.
*
* <UsagePricing
* unit="solicitudes"
* steps={[{ units: 10000, price: 0 }, { units: 100000, price: 49 }, { units: 1000000, price: 299 }]}
* features={["API completa", "Panel de uso"]}
* cta={{ label: "Empezar", href: "/registro" }}
* />
*
* Un paso con price: null se muestra como "A medida" y cambia el botón por contactar.
*/
import { useId, useState, type CSSProperties } from "react";
export type UsageStep = { units: number; price: number | null };
export type UsagePricingProps = {
title?: string;
description?: string;
unit?: string;
steps?: UsageStep[];
currency?: string;
features?: string[];
cta?: { label: string; href: string };
contactCta?: { label: string; href: string };
/** Índice del paso inicial. */
defaultStep?: number;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcpu-root { box-sizing: border-box; display: grid; place-items: center; padding: clamp(28px, 5vw, 72px) clamp(14px, 4vw, 48px); background: radial-gradient(70% 90% at 50% 0%, #1b2554 0%, transparent 70%), #0a0e22; color: #e8ebff; font-family: inherit; }
.gcpu-card { width: min(100%, 760px); box-sizing: border-box; padding: clamp(24px, 4vw, 44px); border: 1px solid rgba(160, 175, 255, .2); border-radius: 28px; background: linear-gradient(180deg, rgba(255, 255, 255, .06), rgba(255, 255, 255, .02)); box-shadow: 0 40px 80px -30px rgba(0, 0, 0, .7); }
.gcpu-card h2 { margin: 0; font-size: clamp(26px, 3.6vw, 40px); font-weight: 700; letter-spacing: -.035em; line-height: 1.08; }
.gcpu-card > p { margin: 10px 0 0; color: #98a2d6; font-size: 16px; line-height: 1.55; }
.gcpu-volume { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-top: 34px; }
.gcpu-volume output { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; }
.gcpu-volume span { color: #98a2d6; font-size: 14px; }
.gcpu-range { --gcpu-pct: 0%; width: 100%; height: 36px; margin: 10px 0 0; background: transparent; -webkit-appearance: none; appearance: none; cursor: pointer; }
.gcpu-range::-webkit-slider-runnable-track { height: 8px; border-radius: 99px; background: linear-gradient(90deg, #7c8bff var(--gcpu-pct), #232b52 var(--gcpu-pct)); }
.gcpu-range::-moz-range-track { height: 8px; border-radius: 99px; background: #232b52; }
.gcpu-range::-moz-range-progress { height: 8px; border-radius: 99px; background: #7c8bff; }
.gcpu-range::-webkit-slider-thumb { -webkit-appearance: none; width: 28px; height: 28px; margin-top: -10px; border: 4px solid #0a0e22; border-radius: 50%; background: #fff; box-shadow: 0 0 0 2px #7c8bff, 0 6px 16px rgba(0, 0, 0, .5); transition: transform .2s cubic-bezier(.34, 1.7, .5, 1); }
.gcpu-range::-moz-range-thumb { width: 22px; height: 22px; border: 4px solid #0a0e22; border-radius: 50%; background: #fff; box-shadow: 0 0 0 2px #7c8bff; }
.gcpu-range:active::-webkit-slider-thumb { transform: scale(1.18); }
.gcpu-range:focus-visible { outline: 2px solid #c9d0ff; outline-offset: 6px; border-radius: 10px; }
.gcpu-marks { display: flex; justify-content: space-between; margin-top: 2px; color: #6c77b0; font-size: 12px; font-variant-numeric: tabular-nums; }
.gcpu-total { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 22px 28px; margin-top: 32px; padding-top: 28px; border-top: 1px solid rgba(160, 175, 255, .16); }
.gcpu-price { display: flex; align-items: baseline; gap: 8px; }
.gcpu-amount { display: inline-block; font-size: clamp(48px, 7vw, 76px); font-weight: 700; letter-spacing: -.055em; line-height: 1; font-variant-numeric: tabular-nums; animation: gcpu-pop .4s cubic-bezier(.16, 1, .3, 1); }
.gcpu-price small { color: #98a2d6; font-size: 16px; }
.gcpu-cta { display: inline-flex; align-items: center; min-height: 54px; padding: 0 30px; border-radius: 16px; background: #fff; color: #0a0e22; font-size: 16px; font-weight: 700; text-decoration: none; transition: transform .3s cubic-bezier(.16, 1, .3, 1), background-color .2s; }
.gcpu-cta:hover { transform: translateY(-2px); background: #dfe3ff; }
.gcpu-cta:focus-visible { outline: 2px solid #c9d0ff; outline-offset: 3px; }
.gcpu-features { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 10px 24px; margin: 26px 0 0; padding: 0; list-style: none; color: #c4cbf5; font-size: 15px; }
.gcpu-features li { display: flex; gap: 10px; } .gcpu-features svg { flex: none; width: 18px; height: 18px; margin-top: 2px; color: #7c8bff; }
@keyframes gcpu-pop { from { opacity: .2; transform: translateY(10px) scale(.96); filter: blur(5px); } }
@media (prefers-reduced-motion: reduce) { .gcpu-amount { animation: none; } .gcpu-range::-webkit-slider-thumb, .gcpu-cta { transition: none; } }
`;
const DEFAULT_STEPS: UsageStep[] = [
{ units: 10_000, price: 0 }, { units: 50_000, price: 19 }, { units: 100_000, price: 39 }, { units: 500_000, price: 149 },
{ units: 1_000_000, price: 249 }, { units: 5_000_000, price: null },
];
const compact = (value: number) => new Intl.NumberFormat("es-MX", { notation: "compact", maximumFractionDigits: 1 }).format(value);
export function UsagePricing({
title = "Paga solo por lo que usas",
description = "Mueve el deslizador y mira cuánto pagarías cada mes. Sin mínimos ni contratos.",
unit = "solicitudes",
steps = DEFAULT_STEPS,
currency = "$",
features = ["Todas las funciones incluidas", "Panel de uso en tiempo real", "Alertas antes de pasarte", "Soporte por correo"],
cta = { label: "Empezar ahora", href: "#registro" },
contactCta = { label: "Hablar con ventas", href: "#contacto" },
defaultStep = 2,
className,
style,
}: UsagePricingProps) {
const rangeId = useId();
const [index, setIndex] = useState(Math.min(defaultStep, steps.length - 1));
const step = steps[index];
const pct = steps.length > 1 ? (index / (steps.length - 1)) * 100 : 0;
const custom = step.price === null;
return (
<>
<style>{CSS}</style>
<section className={["gcpu-root", className].filter(Boolean).join(" ")} style={style}>
<div className="gcpu-card">
<h2>{title}</h2>
<p>{description}</p>
<div className="gcpu-volume"><output htmlFor={rangeId}>{custom ? `Más de ${compact(steps[index - 1]?.units ?? step.units)}` : `Hasta ${compact(step.units)}`} {unit}</output><span>al mes</span></div>
<input
id={rangeId}
className="gcpu-range"
type="range"
min={0}
max={steps.length - 1}
step={1}
value={index}
style={{ ["--gcpu-pct" as string]: `${pct}%` } as CSSProperties}
onChange={(event) => setIndex(Number(event.target.value))}
aria-label={`Volumen mensual de ${unit}`}
aria-valuetext={custom ? "A medida" : `Hasta ${compact(step.units)} ${unit} por ${currency}${step.price} al mes`}
/>
<div className="gcpu-marks" aria-hidden="true"><span>{compact(steps[0].units)}</span><span>{compact(steps[Math.floor(steps.length / 2)].units)}</span><span>{compact(steps[steps.length - 1].units)}+</span></div>
<div className="gcpu-total">
<div className="gcpu-price" aria-live="polite">
<span className="gcpu-amount" key={index}>{custom ? "A medida" : step.price === 0 ? "Gratis" : `${currency}${step.price}`}</span>
{!custom && step.price ? <small>/ mes</small> : null}
</div>
<a className="gcpu-cta" href={custom ? contactCta.href : cta.href}>{custom ? contactCta.label : cta.label}</a>
</div>
<ul className="gcpu-features">
{features.map((feature) => <li key={feature}><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>{feature}</li>)}
</ul>
</div>
</section>
</>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/precios-por-usoEjemplo de uso de Precios por uso con deslizador
Después de copiar usage-pricing.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { UsagePricing } from "./usage-pricing";
export default function Ejemplo() {
return <UsagePricing title="Ejemplo" />;
}Propiedades de UsagePricing
UsagePricing acepta 11 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. |
unit? | string | Opcional. |
steps? | UsageStep[] | Opcional. |
currency? | string | Opcional. |
features? | string[] | Opcional. |
cta? | { label: string; href: string } | Opcional. |
contactCta? | { label: string; href: string } | Opcional. |
defaultStep? | number | Índice del paso inicial. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 129 líneas (8 KB) y define 1 animación con @keyframes. Esto es lo que hace Precios por uso con deslizador 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).
- Expone su estado a tecnologías de apoyo con atributos aria-*.
- Es un componente de cliente de Next.js: incluye la directiva "use client".
Acerca del producto
Precios por uso: un deslizador nativo de volumen mensual con la pista que se llena y un precio que cambia con una animación corta. El último paso puede ser "A medida" y cambia el botón por contactar.
Incluye
Una sección completa en un solo archivo React + TypeScript, con los estilos dentro. No usa Tailwind ni bibliotecas externas.
- Input range nativo: teclado y lectores de pantalla
- Pasos configurables con precio o a medida (price: null)
- Valor accesible con aria-valuetext
- Lista de funciones incluidas
Cómo usarlo
Pasa steps con { units, price }, unit y features. defaultStep elige el paso inicial.