Cómo crear una calculadora de precios por uso en React
Una calculadora de precios por uso en React guarda el valor de cada slider en un array de estado, calcula el total con useMemo (basePrice + suma de value * pricePerUnit) y pasa el resultado a un contador de Framer Motion que anima el número en cada cambio usando useMotionValue y la API imperativa animate().
- Stack: React 18 + Framer Motion 11 + Tailwind v4 + lucide-react, ~163 líneas en total.
- Animación del precio: useMotionValue + animate() con un ease tipo spring [0.16, 1, 0.3, 1] durante 500ms.
- Los sliders son elementos <input type='range'> nativos estilizados con un linear-gradient CSS que sigue dinámicamente el porcentaje de relleno.
- Accesible: los inputs range nativos son navegables por teclado y compatibles con lectores de pantalla sin ninguna configuración.
- Admite EUR y USD; los sliders, la lista de funcionalidades y el texto del CTA son totalmente configurables mediante props.
Pricing Calculator es una sección React donde los usuarios arrastran sliders para definir su uso (puestos, almacenamiento, llamadas a la API, etc.) y ven el precio mensual actualizarse al instante, impulsado por un contador animado. Sustituye las tablas de precios por niveles estáticas por una experiencia directa y táctil que muestra el coste de cada plan antes incluso de que el usuario llegue al CTA.
Anatomía
La sección es una columna centrada max-w-4xl. Un header con fade-in contiene el badge (icono Calculator + label), el h2 y un subtítulo opcional. Debajo, una tarjeta redondeada se divide en dos columnas en pantallas md+: la columna izquierda apila los sliders configurables (label, valor actual, input range nativo, indicaciones min/max); la columna derecha, separada por un borde vertical, muestra el precio animado y el periodo de facturación. Un separador horizontal en la parte inferior de la tarjeta presenta la lista de funcionalidades incluidas como chips inline con iconos Check, y un botón pill de ancho completo cierra la tarjeta.
Cómo funciona
El núcleo del componente es una cadena de reactividad en dos partes. Primero, los handlers onChange de los sliders actualizan un state number[] plano; useMemo recalcula el total cada vez que ese state cambia, de modo que la fórmula queda fuera del ciclo de render. Después, un subcomponente AnimatedPrice recibe el valor calculado como prop y lo pasa a un useMotionValue de Framer Motion. Cuando la prop cambia, un useEffect dispara animate(mv, value, { duration: 0.5, ease }) que lleva la motion value desde su número actual al nuevo. Un segundo useEffect se suscribe a display.on('change') y escribe el entero redondeado directamente en el textContent de un span, esquivando por completo el reconciliador de React para una animación de contador ultra fluida sin re-renders.
Cómo crearlo en React
Modela los sliders como objetos de configuración
Define una interfaz SliderConfig (label, min, max, step, defaultValue, pricePerUnit, unit) y acepta una prop sliders. Inicializa el estado a partir de los valores por defecto para que añadir o quitar un slider nunca requiera tocar la lógica del componente.
const [values, setValues] = useState<number[]>( () => sliders.map((s) => s.defaultValue) );Deriva el total con useMemo
Calcula el precio fuera del JSX con useMemo para que la fórmula solo se vuelva a ejecutar cuando cambian values o las configs de los sliders. Así la función de render queda limpia y la fórmula de coste es fácil de testear de forma unitaria.
const totalPrice = useMemo(() => { return basePrice + sliders.reduce( (sum, s, i) => sum + (values[i] ?? s.defaultValue) * s.pricePerUnit, 0 ); }, [basePrice, sliders, values]);Anima el precio con un contador motion value
Crea AnimatedPrice como subcomponente autónomo. Contiene un useMotionValue(0) y un display = useTransform(mv, Math.round) derivado. Cuando la prop value cambia, llama a la API imperativa animate(mv, value, { duration: 0.5, ease }) de Framer Motion y escribe el resultado directamente en un ref de span, sin state ni re-render.
useEffect(() => { const controls = animate(mv, value, { duration: 0.5, ease: [0.16, 1, 0.3, 1], }); return controls.stop; }, [value, mv]);Estiliza el relleno del range con un gradiente dinámico
Los inputs range nativos ocultan su relleno en la mayoría de los navegadores. Pasa un estilo inline calculado con un linear-gradient que pasa del color de acento al color de borde justo en el porcentaje de relleno del slider. Deriva ese porcentaje de (value - min) / (max - min) * 100 e insértalo en la propiedad background.
style={{ background: `linear-gradient(to right, var(--color-accent) ${pct}%, var(--color-border) ${pct}%)`, }}
Cuándo usarlo
Úsalo en cualquier landing SaaS donde el precio escala con el uso: puestos, llamadas a la API, almacenamiento, ancho de banda. Elimina la incertidumbre de las tablas por niveles y ayuda a los usuarios a autocalificarse antes de contactar con el equipo comercial. Evítalo en productos de tarifa plana simple o freemium donde una tarjeta de precios clásica de 2 columnas es más rápida de leer. En dispositivos táctiles el componente funciona bien arrastrando con el dedo los sliders, pero pruébalo en Safari de iOS, donde el estilo de los range difiere.
Usado por
- Vercel, Usa un slider interactivo para que los usuarios estimen los costes de cómputo y ancho de banda antes de elegir un plan.
- Twilio, Toda la sección de precios es una calculadora de pago por uso; los usuarios introducen volúmenes y ven una estimación de coste en directo.
- Cloudflare, Las páginas de precios de R2 y Workers incluyen estimadores basados en range que actualizan los totales de coste a medida que ajustas el número de solicitudes.
- Lemon Squeezy, Un slider de ingresos estima dinámicamente la comisión de la plataforma, mostrando el porcentaje exacto antes del registro.
FAQ
¿Por qué usar useMemo para el cálculo del precio en lugar de calcularlo dentro de useEffect?
useMemo deriva el precio de forma síncrona durante el render, así que el subcomponente AnimatedPrice siempre recibe el valor correcto en el mismo paint. useEffect se ejecuta después del render, lo que provocaría un desfase de un frame en el que el precio antiguo dispara la animación antes de que llegue el nuevo valor.
¿Cómo evita el contador animado los re-renders de React en cada frame?
AnimatedPrice escribe el entero redondeado directamente en el textContent de un span mediante una suscripción display.on('change'), esquivando por completo el state de React. El nodo del DOM se actualiza a 60fps sin disparar el reconciliador.
¿Puedo añadir un toggle de facturación (mensual/anual) además de los sliders?
Sí. Añade un state billingCycle ('monthly' | 'annual') e inclúyelo en el array de dependencias de useMemo. Multiplica totalPrice por 0,8 (por ejemplo) cuando se selecciona el anual; el contador AnimatedPrice animará la transición automáticamente.
¿Es accesible sin un label visible para cada valor de slider?
El valor actual se muestra en un span visible junto al label del slider, lo que cumple el requisito visual. Para los usuarios de lectores de pantalla, añade aria-label y aria-valuetext a cada <input type='range'> con el valor formateado (p. ej. '5 usuarios') para que el valor se anuncie al cambiar.