Components
53
Accordion Items Article Selection Author Info Basic Carousel Basic Hero Basic Map Blog Pull Out Carousel Carousel Contact Content Accordion Content Carousel Content Image Cta Cta Bar Cta Blocks Cta Collage Event Content Events Grid Example Find Firm Firm Search Firms By Town Gated Content Download Guides Carousel Hero History Homepage Hero Image Content Cta Image List Content Industries Job Content Job Listings Local Firm Carousel Our Firms Pages Carousel Partners Partners Slider People Listing Post Carousel Post Feed Pullquote Section Wrap Service List Split Content Stats Tax Guides Team Grid Title Logos Two Column Video Video Carousel Video Old

Stats

Field
Field Type
Field Name
Instructions
Stats
repeater
block_stats
-- Count Prefix
text
prefix
-- Count
text
count
-- Count Suffix
text
suffix
-- Label
text
label

				
@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.
There are is no readme file with this component.