Typography typographycounteranimatednumberskpi
Animated Counter Text
Smooth number counter animation that counts up from 0 to target value on scroll into viewport.
Preview
0 +
Components Ready to Ship
Usage
Copy the full block below to use this component with its imports.
astro
---
import { AnimatedCounterText } from 'astro-component-kit';
---
<AnimatedCounterText value={15420} prefix="$" suffix="+" duration={1800} /> ---
import { AnimatedCounterText } from 'astro-component-kit';
---
<AnimatedCounterText value={15420} prefix="$" suffix="+" duration={1800} /> Manual Installation
If you are not using the npm package, create a new file src/components/lib/AnimatedCounterText.astro and paste the following code:
astro
---
/**
* 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'.
*/
interface Props {
target?: number;
prefix?: string;
suffix?: string;
label?: string;
color?: string;
}
const {
target = 120,
prefix = '',
suffix = '+',
label = 'Components Ready to Ship',
color = '#d4a017',
} = Astro.props;
---
<div class="stat-counter" data-counter-wrap style={`--counter-color: ${color};`}>
<div class="stat-counter__number">
{prefix && <span class="affix">{prefix}</span>}
<span class="value" data-counter-target={target}>0</span>
{suffix && <span class="affix">{suffix}</span>}
</div>
{label && <p class="stat-counter__label">{label}</p>}
</div>
<style>
.stat-counter {
display: inline-flex;
flex-direction: column;
align-items: center;
font-family: var(--font-sans, system-ui, sans-serif);
user-select: none;
}
.stat-counter__number {
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 {
font-size: 0.85rem;
font-weight: 600;
color: var(--c-text-2, #a3a3a3);
margin-top: 6px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
</style>
<script>
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();
document.addEventListener('astro:page-load', setupCounters);
</script> ---
/**
* 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'.
*/
interface Props {
target?: number;
prefix?: string;
suffix?: string;
label?: string;
color?: string;
}
const {
target = 120,
prefix = '',
suffix = '+',
label = 'Components Ready to Ship',
color = '#d4a017',
} = Astro.props;
---
<div class="stat-counter" data-counter-wrap style={`--counter-color: ${color};`}>
<div class="stat-counter__number">
{prefix && <span class="affix">{prefix}</span>}
<span class="value" data-counter-target={target}>0</span>
{suffix && <span class="affix">{suffix}</span>}
</div>
{label && <p class="stat-counter__label">{label}</p>}
</div>
<style>
.stat-counter {
display: inline-flex;
flex-direction: column;
align-items: center;
font-family: var(--font-sans, system-ui, sans-serif);
user-select: none;
}
.stat-counter__number {
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 {
font-size: 0.85rem;
font-weight: 600;
color: var(--c-text-2, #a3a3a3);
margin-top: 6px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
</style>
<script>
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();
document.addEventListener('astro:page-load', setupCounters);
</script>
Quick Info
- Category
- Typography
- Filename
AnimatedCounterText.astro- Dependencies
- None — pure Astro + CSS
- Tags
- typographycounteranimatednumberskpi