Tipografíatypographycounteranimatednumberskpi
Animated Counter Text
Texto con contador numérico que se anima fluidamente hasta alcanzar el valor objetivo.
Vista Previa en Vivo
0+
Components Ready to Ship
Uso
Copia y pega este código directamente en tu componente React:
tsx
import { AnimatedCounterText } from 'react-component-kit-ui/typography';
<AnimatedCounterText value={15420} prefix="$" suffix="+" duration={1800} />Instalación Manual
Si no estás usando el paquete npm, crea un archivo nuevo y pega el siguiente código: src/components/AnimatedCounterText.tsx + AnimatedCounterText.css:
AnimatedCounterText.tsx
'use client';
import { useEffect, useRef } from 'react';
import type { CSSProperties } from 'react';
import './AnimatedCounterText.css';
/** "a: b; --c: d" → style object */
function st(css: string | undefined): CSSProperties {
const o: Record<string, string> = {};
for (const decl of String(css ?? '').split(';')) {
const i = decl.indexOf(':');
if (i < 0) continue;
const k = decl.slice(0, i).trim();
if (k) o[k.startsWith('--') ? k : k.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = decl.slice(i + 1).trim();
}
return o as CSSProperties;
}
/**
* AnimatedCounterText — Kinetic statistic counter display with animated number progression and unit suffixes.
*
* @param {number} target - Numerical target value (e.g. 99, 120, 2400). Default is 120.
* @param {string} prefix - Optional string prefix (e.g. '$', '+').
* @param {string} suffix - Optional string suffix (e.g. '%', 'k', '+'). Default is '+'.
* @param {string} label - Descriptive text below the counter.
* @param {string} color - Text accent color. Default is '#d4a017'.
*/
export interface AnimatedCounterTextProps {
target?: number;
prefix?: string;
suffix?: string;
label?: string;
color?: string;
}
export function AnimatedCounterText({
target = 120,
prefix = '',
suffix = '+',
label = 'Components Ready to Ship',
color = '#d4a017',
}: AnimatedCounterTextProps) {
// Ported from the Astro <script>: runs once after mount (DOM logic kept as in the Astro kit).
const mounted = useRef(false);
useEffect(() => {
if (mounted.current) return;
mounted.current = true;
function setupCounters() {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const targetEl = entry.target.querySelector('[data-counter-target]') as HTMLElement;
if (!targetEl || targetEl.getAttribute('data-counted')) return;
targetEl.setAttribute('data-counted', 'true');
const finalVal = parseInt(targetEl.getAttribute('data-counter-target') || '0', 10);
const duration = 1500;
const start = performance.now();
function step(now: number) {
const progress = Math.min((now - start) / duration, 1);
// Ease out cubic
const eased = 1 - Math.pow(1 - progress, 3);
targetEl.textContent = Math.floor(eased * finalVal).toLocaleString();
if (progress < 1) {
requestAnimationFrame(step);
} else {
targetEl.textContent = finalVal.toLocaleString();
}
}
requestAnimationFrame(step);
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-counter-wrap]').forEach((el) => observer.observe(el));
}
setupCounters();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
<div className="stat-counter" data-counter-wrap style={st(`--counter-color: ${color};`)} data-rk-89b130="">
<div className="stat-counter__number" data-rk-89b130="">
{prefix && <span className="affix" data-rk-89b130="">{prefix}</span>}
<span className="value" data-counter-target={target} data-rk-89b130="">0</span>
{suffix && <span className="affix" data-rk-89b130="">{suffix}</span>}
</div>
{label && <p className="stat-counter__label" data-rk-89b130="">{label}</p>}
</div>
</>
);
}
export default AnimatedCounterText;AnimatedCounterText.css
/* AnimatedCounterText — ported from astro-component-kit (typography/AnimatedCounterText.astro) */
.stat-counter:where([data-rk-89b130]) {
display: inline-flex;
flex-direction: column;
align-items: center;
font-family: var(--font-sans, system-ui, sans-serif);
user-select: none;
}
.stat-counter__number:where([data-rk-89b130]) {
display: inline-flex;
align-items: baseline;
font-size: clamp(2.5rem, 6vw, 4.2rem);
font-weight: 900;
line-height: 1;
letter-spacing: -0.04em;
color: var(--counter-color);
text-shadow: 0 0 24px rgba(212, 160, 23, 0.35);
}
.stat-counter__label:where([data-rk-89b130]) {
font-size: 0.85rem;
font-weight: 600;
color: var(--c-text-2, #a3a3a3);
margin-top: 6px;
text-transform: uppercase;
letter-spacing: 0.05em;
}Información Rápida
- Categoría
- Tipografía
- Nombre de archivo
AnimatedCounterText.tsx- Dependencias
- Ninguna — React + CSS puro
- Etiquetas
- typographycounteranimatednumberskpi