@import "../../resources/scss/util/variables";
@import "../../resources/scss/util/mixins";
.block-stats {
&.has-background:not(.has-light-blue-background-color) {
color: $white;
}
&__stat {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 2rem 0;
@include bp($md) {
padding: rem-calc(64) 0;
justify-content: center;
align-items: center;
}
}
&__count {
@include responsive-font($heading-3, 64px);
font-weight: 500;
opacity: 0;
}
&__count-number {
// opacity: 0;
transition: opacity 0.3s ease-in-out;
&.fade-in {
opacity: 1;
}
}
&__label {
font-size: $body-large;
@include bp($md) {
text-align: center;
}
}
}
// Vanilla JavaScript implementation with no dependencies
class Stats {
constructor(block) {
this.block = block;
this.stats = null;
this.init();
}
init() {
// Select all stat containers
this.statContainers = this.block.querySelectorAll('.block-stats__count');
this.statContainers.forEach(container => {
// Find the number element
const numberElement = container.querySelector('.block-stats__count-number');
if (!numberElement) return;
// Initialize to 0
numberElement.textContent = '0';
// Hide the entire container initially
container.style.opacity = '0';
container.style.transition = 'opacity 0.5s ease-out';
});
// Create IntersectionObserver for the block
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.startAnimations();
observer.unobserve(this.block); // Stop observing after animation
}
});
}, {
root: null,
rootMargin: '0px 0px 100px 0px', // Trigger 100px before element enters viewport
threshold: 0.01 // Trigger when just 1% of the element is visible
});
observer.observe(this.block);
}
startAnimations() {
// Start animations with staggered timing
this.statContainers.forEach((container, index) => {
// Find the number element
const numberElement = container.querySelector('.block-stats__count-number');
if (!numberElement) return;
const countTo = parseInt(numberElement.getAttribute('data-count'));
if (!countTo && countTo !== 0) return;
// Create a staggered start for each stat
setTimeout(() => {
// Fade in the entire container (number + suffix)
container.style.opacity = '1';
// Start counting animation for just the number
this.animateCounter(numberElement, 0, countTo, 2000);
}, index * 500); // 500ms stagger between stats
});
}
animateCounter(element, start, end, duration) {
const startTime = performance.now();
const updateCounter = (currentTime) => {
// Calculate how far along we are in the animation (0 to 1)
const elapsed = Math.min(currentTime - startTime, duration);
const progress = elapsed / duration;
// Apply easing function
const easedProgress = this.customEase(progress);
// Calculate the current value
const currentValue = Math.floor(start + easedProgress * (end - start));
// Update only the number part
element.textContent = currentValue;
// Continue animation if not complete
if (progress < 1) {
requestAnimationFrame(updateCounter);
}
};
requestAnimationFrame(updateCounter);
}
customEase(t) {
// Custom easing function that slows down near the end
// This creates a more dramatic deceleration effect
return 1 - Math.pow(1 - t, 3); // Cubic ease-out
}
}
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.block-stats').forEach((block) => {
new Stats(block);
});
});
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "strategiq/stats",
"title": "Stats",
"description": "Example block to be used as a template",
"category": "strategiq",
"icon": "strategiq",
"acf": {
"mode": "preview",
"renderTemplate": "block-stats.php"
},
"supports": {
"anchor": true,
"align": false,
"color": {
"background": true,
"text": false,
"gradients": true
},
"spacing": {
"padding": [
"top",
"bottom"
],
"margin": [
"top",
"bottom"
]
}
},
"example": {
"attributes": {
"mode": "preview",
"data": {
"heading_type": "h2",
"heading_text": "Example - Stats",
"content": "This is some example content to represent what the content will look like"
}
}
},
"style": ["file:../../assets/css/stats/block-stats.css"],
"viewScript": ["stats"]
}
This component is not currently used on any pages.