@use 'sprucecss/scss/spruce' as *;
.l-number {
&__inner {
--inline-size: 15rem;
@include layout-flex('l');
}
}
.number-card {
@include layout-stack('xs');
border-bottom: 1px solid color('border');
padding-block-end: spacer('l');
&__data {
color: color('primary');
font-family: config('font-family-heading', $typography);
font-size: responsive-font-size(5rem);
font-weight: 700;
line-height: 1;
}
}
<div class="number-card">
<p class="number-card__data">
<span data-action="countup" data-target="325">0</span>+
</p>
<p class="number-card__caption">Projects Completed</p>
</div>
<div class="number-card">
<p class="number-card__data">
<span data-action="countup" data-target="150">0</span>+
</p>
<p class="number-card__caption">Happy Clients</p>
</div>
<div class="number-card">
<p class="number-card__data">
<span data-action="countup" data-target="25">0</span>+
</p>
<p class="number-card__caption">Awards Won</p>
</div>
<div class="number-card">
<p class="number-card__data">
<span data-action="countup" data-target="100">0</span>+
</p>
<p class="number-card__caption">Cups of Coffee Per Day</p>
</div>
(() => {
const counters = document.querySelectorAll('[data-action="countup"]');
const duration = 3500;
const countUp = (element, target) => {
let current = 0;
let startTime = null;
const updateCounter = (timestamp) => {
if (!startTime) startTime = timestamp;
const progress = timestamp - startTime;
const easing = 1 - (1 - progress / duration) ** 3;
current = Math.min(target, target * easing);
element.textContent = Math.floor(current);
if (progress < duration) {
requestAnimationFrame(updateCounter);
} else {
element.textContent = target;
}
};
requestAnimationFrame(updateCounter);
};
const observerInit = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const counter = entry.target;
const target = Number(counter.getAttribute('data-target'));
countUp(counter, target);
observer.unobserve(counter);
}
});
}, { threshold: 0.5 });
counters.forEach((counter) => observerInit.observe(counter));
})();