Contador tragamonedas
Cada dígito rueda hacia el nuevo valor con curva elástica.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
/**
* Contador tragamonedas: muestra un número donde cada dígito rueda hacia arriba dentro de su
* propia columna cuando el valor cambia. La curva es elástica para que el aterrizaje se sienta
* físico, y se suaviza con prefers-reduced-motion.
*
* <SlotCounter value={1234} prefix="$" suffix=" MXN" />
*/
import { useEffect, useRef, useState } from "react";
const CSS = `
.gslt-counter { display: inline-flex; align-items: baseline; font-feature-settings: "tnum" 1, "lnum" 1; font-variant-numeric: tabular-nums; font-family: inherit; line-height: 1; }
.gslt-cell { position: relative; display: inline-block; height: 1em; line-height: 1; overflow: hidden; vertical-align: baseline; min-width: 0.62em; }
.gslt-rail { display: flex; flex-direction: column; transition: transform .55s cubic-bezier(.34, 1.36, .64, 1); will-change: transform; }
.gslt-digit { display: block; height: 1em; line-height: 1; text-align: center; }
.gslt-affix { display: inline-block; }
.gslt-space { display: inline-block; width: 0.32em; }
.gslt-sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
@media (prefers-reduced-motion: reduce) {
.gslt-rail { transition: none; }
}
`;
export function SlotCounter({ value, prefix, suffix, className, style }: { value: number; prefix?: string; suffix?: string; className?: string; style?: React.CSSProperties }) {
const initial = Math.max(0, Math.abs(Math.trunc(value)));
const [display, setDisplay] = useState(initial);
const maxDigitsRef = useRef(String(initial).length);
useEffect(() => {
setDisplay(Math.max(0, Math.abs(Math.trunc(value))));
maxDigitsRef.current = Math.max(maxDigitsRef.current, String(Math.abs(Math.trunc(value))).length);
}, [value]);
const total = display;
const width = Math.max(maxDigitsRef.current, String(total).length);
const padded = String(total).padStart(width, " ");
return (
<>
<style>{CSS}</style>
<span className={["gslt-counter", className].filter(Boolean).join(" ")} style={style} aria-hidden="true">
{prefix ? <span className="gslt-affix">{prefix}</span> : null}
{padded.split("").map((char, index) => {
if (char === " ") return <span key={index} className="gslt-space" />;
const digit = Number(char);
return (
<span key={index} className="gslt-cell">
<span className="gslt-rail" style={{ transform: `translateY(-${digit}em)` }}>
{Array.from({ length: 10 }, (_, n) => (
<span key={n} className="gslt-digit">{n}</span>
))}
</span>
</span>
);
})}
{suffix ? <span className="gslt-affix">{suffix}</span> : null}
</span>
<span className="gslt-sr" aria-live="polite">
{prefix ?? ""}{value.toLocaleString("es-MX")}{suffix ? ` ${suffix}` : ""}
</span>
</>
);
}
/** Vitrina: cuenta desde 0 hasta un valor objetivo con easing, repitiendo al pulsar. */
export function SlotCounterDemo({ end = 12480, prefix = "$", suffix = " MXN" }: { end?: number; prefix?: string; suffix?: string }) {
const [value, setValue] = useState(0);
useEffect(() => {
let raf: number | null = null;
const start = performance.now();
const duration = 2200;
function tick(now: number) {
const t = Math.min(1, (now - start) / duration);
const eased = 1 - Math.pow(1 - t, 3);
setValue(Math.round(end * eased));
if (t < 1) raf = requestAnimationFrame(tick);
}
raf = requestAnimationFrame(tick);
return () => {
if (raf !== null) cancelAnimationFrame(raf);
};
}, [end]);
return <SlotCounter value={value} prefix={prefix} suffix={suffix} style={{ fontSize: 56, fontWeight: 700, letterSpacing: "-.04em", color: "#f0f2f6" }} />;
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/slot-counterEjemplo de uso de Contador tragamonedas
Después de copiar slot-counter.tsx a tu proyecto, impórtalo y colócalo donde lo necesites. Exporta 2 componentes: SlotCounter, SlotCounterDemo.
import { SlotCounter } from "./slot-counter";
export default function Ejemplo() {
return <SlotCounter />;
}Accesibilidad y rendimiento
El archivo tiene 87 líneas (4 KB) y no necesita @keyframes. Esto es lo que hace Contador tragamonedas por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Expone su estado a tecnologías de apoyo con atributos aria-*.
- Anima con requestAnimationFrame, sincronizado con el refresco de pantalla.
- Es un componente de cliente de Next.js: incluye la directiva "use client".
Acerca del producto
Un número en el que cada dígito vive en su propia columna vertical. Cuando value cambia, esa columna desliza hasta revelar el nuevo dígito con una curva elástica. Usa cifras tabulares para que el ancho no salte, y muestra un valor estable a lectores de pantalla.
Incluye
Un solo archivo React + TypeScript, con los estilos dentro. No usa Tailwind ni bibliotecas externas.
- Cada dígito rueda hacia arriba con curva elástica
- Cifras tabulares (anchura fija por dígito)
- Prefijo y sufijo opcionales
- Valor estable expuesto a lectores de pantalla
- Cumple prefers-reduced-motion
Cómo usarlo
Pasa value como número. Opcionalmente prefix y suffix. El ancho reservado es el del valor con más dígitos desde el montaje.