Want to add animated numbers to your Webflow site? While Webflow offers extensive design tools, number animations aren't included out of the box. This guide shows you how to implement the BRIX Templates Counter attribute, a straightforward solution that brings dynamic number animations to your site with no code.
Whether you're showcasing statistics, tracking goals, or adding dynamic elements to your site, you'll learn how to implement and customize number animations effectively.
Animated counters serve multiple practical purposes in modern web design:
By animating these values instead of displaying them statically, you create a more engaging experience that naturally draws attention to important metrics.
Once you are in there, insert the BRIX Templates Counter attribute script below
<script>
/*!
* BRIX Templates Number Counter Attribute for Webflow
* ----------------------------------------------------------------------------
* Creates animated number counters using a simple attribute system. Perfect for
* statistics, metrics, and dynamic number displays.
*
* Two Ways to Use:
* 1. Simple Version:
* [brix-counter="start:0;end:100"]
*
* 2. Advanced Version:
* [brix-counter="start:0;end:200;duration:3;format:comma;trigger:scroll"]
*
* Supported Options:
* start: Where to begin counting (required)
* end: Where to stop counting (required)
* duration: How long to animate (in seconds)
* step: Count by specific numbers (e.g., by 5s or 10s)
* format: How to show thousands ("comma", "period", "none")
* prefix: Add text before the number ("$", "€", etc.)
* suffix: Add text after the number ("+", "k", etc.)
* trigger: When to start ("load", "scroll", "click")
* delay: Wait time before starting
* easing: Animation style ("linear", "easeIn", "easeOut", "easeInOut")
* replay: Animation behavior ("once", "multiple")
* direction: Count direction ("normal", "inverse")
*
* Version: 1.0.1
* Author: BRIX Templates
*/
(function() {
'use strict';
// -------------------------------------------------------------
// 1) Easing functions
// -------------------------------------------------------------
const EASING = {
linear: t => t,
easeIn: t => t * t,
easeOut: t => t * (2 - t),
easeInOut(t) {
return (t < 0.5) ? 2*t*t : -1 + (4 - 2*t) * t;
},
};
function getEasingFunction(name) {
return EASING[name] || EASING.linear;
}
// -------------------------------------------------------------
// 2) On DOM Ready
// -------------------------------------------------------------
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initBrixCounters);
} else {
initBrixCounters();
}
// -------------------------------------------------------------
// 3) Main function: find all [brix-counter] elements
// -------------------------------------------------------------
function initBrixCounters() {
const counters = document.querySelectorAll('[brix-counter]');
if (!counters.length) return;
counters.forEach((el) => {
setupCounter(el);
});
}
// -------------------------------------------------------------
// 4) Parse attribute => set up each counter
// -------------------------------------------------------------
function setupCounter(el) {
const rawAttr = el.getAttribute('brix-counter') || '';
const config = parseCounterParams(rawAttr);
// read config with defaults
let {
start, end, duration, step, format, prefix, suffix,
trigger, delay, easing, replay, direction
} = config;
start = parseFloat(start) || 0;
end = parseFloat(end) || 100;
duration = parseFloat(duration) || 2;
step = parseFloat(step) || 1;
format = format || 'none';
prefix = prefix || '';
suffix = suffix || '';
trigger = trigger || 'load';
delay = parseFloat(delay) || 0;
easing = easing || 'linear';
replay = replay || 'once';
direction= direction|| 'normal';
// if direction=inverse => swap start & end
let realStart = start;
let realEnd = end;
if (direction === 'inverse') {
realStart = end;
realEnd = start;
}
// set initial text as realStart
el.textContent = formatNumber(realStart, step, format, prefix, suffix, realStart, realEnd);
// define an animate function
let hasRun = false;
function animateCounter() {
// skip if replay=once and has run already
if (hasRun && replay === 'once') return;
hasRun = true;
const startTime = performance.now();
const durationMs = duration * 1000;
const delayMs = delay * 1000;
const easeFn = getEasingFunction(easing);
// after the delay, do an anim loop
// show the start number during the delay
setTimeout(() => {
const animStart = performance.now(); // the moment we actually start anim
function frame(now) {
const elapsed = now - animStart;
let progress = elapsed / durationMs;
if (progress < 0) progress = 0;
if (progress > 1) progress = 1;
const eased = easeFn(progress);
const rawVal = realStart + (realEnd - realStart) * eased;
const stepped = stepRound(rawVal, step, realStart, realEnd);
el.textContent = formatNumber(stepped, step, format, prefix, suffix, realStart, realEnd);
if (progress < 1) {
requestAnimationFrame(frame);
}
}
requestAnimationFrame(frame);
}, delayMs);
}
// attach triggers
switch(trigger) {
case 'load':
animateCounter();
break;
case 'scroll':
attachScrollTrigger(el, animateCounter, replay);
break;
case 'click':
el.addEventListener('click', animateCounter);
break;
default:
animateCounter();
break;
}
}
// -------------------------------------------------------------
// 5) parse "key:value; key:value; ..."
// -------------------------------------------------------------
function parseCounterParams(str) {
const parts = str.split(';');
const conf = {};
parts.forEach(part => {
const trimmed = part.trim();
if (!trimmed) return;
const [k, v] = trimmed.split(':');
if (!k || v === undefined) return;
conf[k.trim()] = v.trim();
});
return conf;
}
// -------------------------------------------------------------
// 6) Round rawVal to nearest multiple of step
// -------------------------------------------------------------
function stepRound(rawVal, step, minVal, maxVal) {
if (step <= 1) {
return rawVal;
}
const ratio = rawVal / step;
const rounded = Math.round(ratio) * step;
if (minVal < maxVal) {
return Math.max(minVal, Math.min(maxVal, rounded));
} else {
// if inverse scenario
return Math.min(minVal, Math.max(maxVal, rounded));
}
}
// -------------------------------------------------------------
// 7) formatNumber
// -------------------------------------------------------------
function formatNumber(val, step, format, prefix, suffix, minVal, maxVal) {
const n = Math.floor(val);
let out = n.toString();
if (format === 'comma') {
out = out.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
} else if (format === 'period') {
out = out.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
}
return prefix + out + suffix;
}
// -------------------------------------------------------------
// 8) IntersectionObserver for scroll triggers
// if replay=multiple => keep observing
// -------------------------------------------------------------
function attachScrollTrigger(el, callback, replayMode) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
callback();
if (replayMode === 'once') {
observer.unobserve(el);
}
}
});
}, { threshold: 0.2 });
observer.observe(el);
}
})();
</script>
If you prefer to use the counter on specific pages only, you can alternatively add it through:
After adding the script, you can set up individual counter elements:
The brix-counter Webflow attribute accepts various parameters in a single string, separated by semicolons. Here are the key configuration options:
brix-counter="start:0;end:200;duration:3;step:10;format:comma;prefix:$;suffix:+;trigger:scroll;delay:2;easing:easeInOut;replay:multiple;direction:normal"
To simplify the configuration process, you can use our Counter Generator tool:
After generating your counter configuration, paste the complete code into the 'value' field of your brix-counter attribute in the Webflow element settings.
You can follow these same steps to create additional counters throughout your site—each with its own unique configuration. Every counter can have different starting points, animations, and triggers while using the same base script.
If your counter isn't working as expected, check these common solutions:
The BRIX Templates Counter attribute brings powerful number animation capabilities to Webflow with minimal setup requirements. Whether you're creating engaging statistics displays, dynamic fundraising trackers, or interactive countdowns, this solution provides the flexibility and reliability you need while maintaining clean, professional design standards.
Need help implementing more advanced counter scenarios or looking to explore custom animation solutions? Our specialized Webflow agency can help create tailored implementations that perfectly match your specific needs. Feel free to reach out to discuss how we can enhance your Webflow project with sophisticated counter animations and other custom features.
Learn how to add a copy URL button to your Webflow site. Step-by-step guide for implementing easy page sharing without plugins or code.
Learn how to add copy-to-clipboard button to your Webflow site in 5 minutes or less.
Complete tutorial explaining how to prevent users from specific countries from accessing your Webflow website.