Hey, it seems that no one is interested in this – yet i also had the same issue.
Repeat Counters when re-entering View (Native Widget Hook) :
- Add a Code Widget underneath your last counter, or at bottom of the page.
- Add this to the Javascript part and run it:
document.addEventListener('DOMContentLoaded', () => {
const debug = false; // if true= shows console logs
const enterThreshold = 0.45;
const SELECTOR = '.brxe-counter[data-bricks-counter-options]';
const counters = Array.from(document.querySelectorAll(SELECTOR));
const logger = {
log(...args) {
if (!debug) return;
console.log('[Bricks Counter]', ...args);
},
warn(...args) {
if (!debug) return;
console.warn('[Bricks Counter]', ...args);
},
init(items) {
if (!debug) return;
console.groupCollapsed(`[Bricks Counter] Initialized ${items.length} counter(s)`);
items.forEach(item => {
console.log({
id: item.id,
from: item.from,
to: item.to,
duration: item.duration,
separator: item.separator,
grouping: item.useGrouping
});
});
console.groupEnd();
}
};
if (!counters.length) {
logger.warn('No counters found.');
return;
}
const parseOptions = (el) => {
try {
return JSON.parse(el.getAttribute('data-bricks-counter-options') || '{}');
} catch (error) {
logger.warn('Invalid data-bricks-counter-options JSON', el, error);
return null;
}
};
const formatNumber = (value, separator = '.', useGrouping = true) => {
const num = Math.round(value);
if (!useGrouping) return String(num);
return String(num).replace(/\B(?=(\d{3})+(?!\d))/g, separator);
};
const items = counters.map((el, index) => {
const options = parseOptions(el);
if (!options) return null;
const countEl = el.querySelector('.count');
if (!countEl) {
logger.warn('Missing .count element', el);
return null;
}
const item = {
id: el.dataset.scriptId || el.id || `counter-${index + 1}`,
el,
countEl,
from: Number(options.countFrom ?? 0),
to: Number(options.countTo ?? 0),
duration: Number(options.duration ?? 1000),
separator: options.separator ?? '.',
useGrouping: !!options.thousands,
hasPlayedInView: false,
isAnimating: false,
raf: null
};
countEl.textContent = formatNumber(item.from, item.separator, item.useGrouping);
return item;
}).filter(Boolean);
if (!items.length) {
logger.warn('Counters found, but none could be initialized.');
return;
}
logger.init(items);
const itemMap = new Map(items.map(item => [item.el, item]));
const resetCounter = (item) => {
if (item.raf) {
cancelAnimationFrame(item.raf);
item.raf = null;
}
item.isAnimating = false;
item.countEl.textContent = formatNumber(item.from, item.separator, item.useGrouping);
};
const animateCounter = (item) => {
if (item.isAnimating || item.hasPlayedInView) return;
item.isAnimating = true;
item.hasPlayedInView = true;
logger.log('enter → play', item.id);
const start = performance.now();
const delta = item.to - item.from;
const tick = (now) => {
const progress = Math.min((now - start) / item.duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
const current = item.from + delta * eased;
item.countEl.textContent = formatNumber(current, item.separator, item.useGrouping);
if (progress < 1) {
item.raf = requestAnimationFrame(tick);
return;
}
item.countEl.textContent = formatNumber(item.to, item.separator, item.useGrouping);
item.isAnimating = false;
item.raf = null;
};
item.raf = requestAnimationFrame(tick);
};
const unlockCounter = (item) => {
if (!item.hasPlayedInView && !item.isAnimating) return;
resetCounter(item);
item.hasPlayedInView = false;
logger.log('fully left viewport → reset', item.id);
};
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
const item = itemMap.get(entry.target);
if (!item) return;
const ratio = entry.intersectionRatio || 0;
// start only when enough is visible
if (ratio >= enterThreshold) {
animateCounter(item);
return;
}
// reset only when fully out of view
if (ratio === 0) {
unlockCounter(item);
}
});
}, {
threshold: [0, enterThreshold],
root: null,
rootMargin: '0px'
});
items.forEach(item => observer.observe(item.el));
});
You also have some settings there, but i won’t go into details, as for most users this is enough.
Hope it helps.
Greetings from Austria