Counter questions: 1) repeat 2) countdown?

Hi,

A couple of questions about the Counter.

First, how can you get it to repeat/reload when people return to that part of the page? Essentially the unanswered question in this post:

Second, is there a way to count down numbers, not dates? For example, to go from 100 to 1? It seems to only increment positively, and entering a negative number as a starting point doesn’t help…

Any suggestions welcome and appreciated…

Looks like this forum doesn’t get much attention.

Makes a real difference to choosing which product to go with…

Did you find any resolution for this issue?

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