Animated Counter with JavaScript

numeric counter

Learn how to make a simple animated counter, using a WordPress PHP shortcode for the back-end, and some JavaScript for the front-end. It’s a simple way to draw attention to key statistics without being overly flashy.

--
--

We’ll write a small PHP function, a little bit of JavaScript, then style it with some CSS. We’ll also be careful to only include our CSS and JS files on pages that need them (pages with counters on them) so we don’t unnecessarily slow-down the rest our website (pages without counters on them).

We’re going to keep our PHP, JavaScript and CSS files in a self-contained module so it’s easy to use in multiple projects. What I mean is… we’re not just going to paste loads and loads of code into the theme’s main functions.php file – we’re going to keep our theme’s functions.php nice and tidy.

Get Started with Placeholder Files

In your custom child theme‘s folder, create a file called animated-counter.php and a subfolder called animated-counter. In the subfolder, create two more empty files, called afc-frontend.css and afc-frontend.js.

  • animated-counter.php (PHP code)
  • animated-counter (folder)
    • afc-frontend.css (CSS stylesheet)
    • afc-frontend.js (JavaScript code)
  • …the rest of the files in your custom child theme…

animated counter frontend files
The animated-counter subfolder

We’re going to put our custom PHP code inside a shortcode. It’s a bit of an old-fashioned way of doing things (we should really use a Gutenberg Block) but shortcodes are quick, easy and lightweight. Great for adding reusable PHP snippets like this.

The PHP Code

Edit animated-counter.php and paste the following into it.

<?php

/**
 * Animated Frontend Counter.
 *
 * https://wp-tutorials.tech/add-functionality/animated-counter-with-javascript/
 */

defined('WPINC') || die();

/**
 * Timings are in milliseconds (ms). 1sec = >1000ms
 */
const ACF_ANIM_DURATION = 1500; // ms.
const ACF_ANIM_INTERVAL = 25;   // ms.

function do_shortcode_afc_counter($atts = null) {
	global $afc_have_assets_been_queued;

	// Extract the shortcodes' arguments.
	$args = shortcode_atts(
		array(
			'start' => 0,
			'end' => 100,
			'label' => null,
		),
		$atts
	);

	// Start rendering our HTML.
	$html = '<div class="afc-container">';

	// Render the optional label.
	if (!empty($args['label'])) {
		$html .= sprintf('<label>%s</label>', esc_html($args['label']));
	}

	// Render the counter.
	$html .= sprintf(
		'<span data-counter-anim="%s">--</span>',
		esc_attr(json_encode($args))
	);

	// Finish rendering our HTML.
	$html .= '</div>'; // .afc-container

	// If this is the first acf_counter shortcode then enqueue our CSS and JS
	// files, so WordPress can add them to the document footer for us.
	if (is_null($afc_have_assets_been_queued)) {
		$base_url = get_stylesheet_directory_uri() . '/' . pathinfo(__FILE__, PATHINFO_FILENAME) . '/';
		$theme_version = wp_get_theme()->get('Version');

		// Our CSS styles asset.
		wp_enqueue_style('afc-frontend', $base_url . 'afc-frontend.css', null, $theme_version);

		// Our JavaScript asset.
		wp_register_script('afc-frontend', $base_url . 'afc-frontend.js', array('jquery'), $theme_version, true);
		wp_localize_script(
			'afc-frontend',
			'acfCounters',
			array(
				'animDuration' => ACF_ANIM_DURATION,
				'animInterval' => ACF_ANIM_INTERVAL,
			)
		);
		wp_enqueue_script('afc-frontend');

		$afc_have_assets_been_queued = true;
	}

	return $html;
}
add_shortcode('afc_counter', 'do_shortcode_afc_counter');

The main function for dealing with the shortcode and creating the HTML works like this:

  1. Parse the shortcode’s arguments, like “label”, “start” and “end”.
  2. Open our containing <div>.
  3. If we’ve specified a label, then add it to the HTML now.
  4. Add a <span> element to hold the actual counter.
  5. Close the <div> container.
  6. Tell WordPress about our CSS and JavaScript files (assets) so it can add them to the document footer.

Now we’ve got a PHP function in there to render the shortcode, we need to tell our custom child theme about it. In your custom child theme, open functions.php and add the following:

/**
 * Shortcode for animated frontend counters.
 */
require_once dirname(__FILE__) . '/animated-counter.php';

That should be enough to make things sort-of work. Try adding a shortcode block to some content. It’ll all look a bit rubbish and it won’t animate, because we’ve not written any CSS/JS, but it should confirm that the PHP side of things is plumbed-in properly.

Animated JavaScript counter shortcode
Example shortcode usage

The JavaScript Animation

For the frontend JavaScript stuff (the code to animate the counters) we’re going to rely on jQuery. This will make the code easier-to-read than if we just did it 100% from scratch.

Open the afc-frontend.js file and paste the following into it:

(function($) {
    'use strict';

    $(window).on('load', function() {
        console.log('Animated Frontend Counters : load');

        // Only run our code if acfCounters has been set in the HTML by the
        // WordPress wp_localize_script() function.
        if (typeof acfCounters != 'undefined') {
            var startTime = null;
            var counterElements = [];
            var isAnimationStarted = false;

            // the "observer" responds when one of our counter animations
            // becomes 100% visible on the page.
            var observer = new IntersectionObserver(function(entries) {
                if (!isAnimationStarted) {
                    entries.forEach((entry) => {
                        if (entry.isIntersecting === true) {
                            isAnimationStarted = true;
                        }
                    });

                    if (isAnimationStarted) {
                        console.log('Starting counter animations now');
                        counterIteration();
                    }
                }
            }, { threshold: [1] });

            // Look for the <span> elements that have a data-counter-anim
            // property, stash them in the counterElements array and add them
            // to the observer.
            $('[data-counter-anim]').each(function(index, el) {
                var definition = $(this).data('counter-anim');
                counterElements.push(this);
                observer.observe($(this).closest('.afc-container')[0]);
            });

            function counterIteration() {
            	// If this is the first iteration, the set the start time to now.
                if (startTime == null) {
                    startTime = new Date();
                }

                var now = new Date();
                var timeSinceStart = now - startTime;
                var percentage = timeSinceStart / acfCounters.animDuration;
                var isLastIteration = (percentage >= 1.00);

                counterElements.forEach(counterElement => {
                    var definition = $(counterElement).data('counter-anim');
                    var counterStart = parseInt(definition.start);
                    var counterEnd = parseInt(definition.end);
                    var currentCount = counterStart;

                    if (isLastIteration) {
                        currentCount = counterEnd;
                    } else {
                        currentCount = Math.round(counterStart + (percentage * (counterEnd - counterStart)));
                    }

                    $(counterElement).text(currentCount);
                });

                if (!isLastIteration) {
                    setTimeout(counterIteration, acfCounters.animInterval);
                }
            }
        }
    });
})(jQuery);

The logic here runs like this:

  1. If the global variable acfCounters has been defined (by the wp_localize_script function in PHP) then search the DOM for elements that have a data-counter-anim property set.
    1. Stash these elements in an array called counterElements.
    2. Add each element to the observable, which will tell us when any of our counter animation <div> elements becomes visible on the page.
  2. If one or more of our counter animation elements becomes visible, and we’ve not started running the animations yet, then start now.
  3. For each animation iteration…
    1. Figure out what percentage of the animation we’re at right now, based on the current time and the time we started. 100% means we’re about to render the final frame of the animation.
    2. Loop through the elements in counterElements, figure out the current value to display, then call the element’s text() function.
    3. If we’re not at the end of the animation, run another iteration after a timeout of animInterval.

After you’ve saved the JavaScript file… reload your test page and you should see the animation working.

infoIf you don’t see the animation then open your browser’s development tools and look at your JavaScript error console. Perhaps a file name has been misspelled, so your browser can’t find your JavaScript file?

Add Some Style & Test It

The last bit is easy. Paste the following into your afc-frontend.css to get yourself started with the styling. The HTML for the counter is simply structured so it should be easy to customise it for your site.

/**
 * Animated Frontend Counters
 */

.afc-container {
    background-color: #202020;
    background-image: linear-gradient(to bottom, transparent, #303030);
    color: white;
    text-align: center;
    border-radius: 0.5rem;
    overflow: hidden;
    margin-bottom:  1rem;
}

.afc-container label {
    font-size: 16pt;
    display: block;
    letter-spacing: 0.1em;
    background-image: linear-gradient(to bottom, transparent -50%, #303030);
    border-bottom: 1px dotted #606060;
    padding: 1.25rem 0;
    opacity: 0.80;
}

.afc-container [data-counter-anim] {
    display: block;
    font-family: monospace;
    font-size: 24pt;
    letter-spacing: 0.1em;
    padding: 1.25rem;
}

That should do it. Save the changes to your CSS file, reload your test content and you should be sorted. If something’s not working, then keep an eye on your browser’s JavaScript console for errors… missing files and typos.

Happy counter-ing! 👍

Like This Tutorial?

Let us know

WordPress plugins for developers

2 thoughts on “Animated Counter with JavaScript”

    • I’ve been thinking about making a tutorial on how to make a Gutenberg Block. The animated counter would be a good one to use – good idea. I’ll add it to my list.

      It will be a couple of weeks though, as I’ve already got the idea planned for the next tutorial. I’ll see if I can get this written in early February for you.

      Reply

Leave a comment