Counter Up

Biblioteca JavaScript pura para animação de contadores numéricos

Zero dependências ESM + UMD Headless / SSR IntersectionObserver Node.js

Básico padrão

Uso mínimo: passe o seletor e o valor final. Todos os outros parâmetros são opcionais.

Contador simples
Usuários ativos
0
counterUp("#b1", {
  end: 48230,
  duration: 2000,
});
Comparação de duração — rápido / padrão / lento
800ms
0
2000ms
0
4500ms
0
counterUp("#b-fast", {
  end: 1000,
  duration: 800,   // rápido
});

counterUp("#b-mid", {
  end: 1000,
  duration: 2000,  // padrão
});

counterUp("#b-slow", {
  end: 1000,
  duration: 4500,  // lento
});
Duração zero — pula direto para o valor final
0
const c = counterUp("#b-zero", {
  end: 99999,
  duration: 0,     // sem animação
  autostart: false,
});

btn.onclick = () => c.start();

Formatação prefix / suffix / decimals

Use prefix, suffix e decimals para qualquer formato sem precisar de um formatter personalizado.

Moeda — R$ com 2 decimais
Receita total
R$ 0,00
counterUp("#f-currency", {
  start: 0,
  end: 128750.90,
  duration: 2200,
  decimals: 2,
  prefix: "R$ ",
  locale: "pt-BR",
});
Porcentagem
Satisfação
0%
Uptime
0%
counterUp("#f-pct1", {
  end: 97,
  suffix: "%",
  duration: 1600,
  useGrouping: false,
});

counterUp("#f-pct2", {
  end: 99.98,
  suffix: "%",
  decimals: 2,
  duration: 1800,
  useGrouping: false,
});
Outros formatos — pontos, temperatura, distância
Pontos
0
Temperatura
0
Distância
0
counterUp("#f-pts", {
  end: 8450,
  suffix: " pts",
  duration: 1800,
});

counterUp("#f-temp", {
  end: 36.6,
  suffix: " °C",
  decimals: 1,
  duration: 1500,
  useGrouping: false,
});

counterUp("#f-km", {
  end: 42.195,
  suffix: " km",
  decimals: 3,
  duration: 2000,
  useGrouping: false,
});
Separador de milhar — useGrouping
Com agrupamento
0
Sem agrupamento
0
counterUp("#f-grp1", {
  end: 1234567,
  useGrouping: true,  // → 1.234.567
  duration: 2000,
});

counterUp("#f-grp2", {
  end: 1234567,
  useGrouping: false, // → 1234567
  duration: 2000,
});

Locale Intl.NumberFormat

A opção locale controla os separadores decimal e de milhar via Intl.NumberFormat.

Mesmo número em quatro locales
pt-BR
0
en-US
0
de-DE
0
fr-FR
0
const opts = {
  end: 1234567.89,
  decimals: 2,
  duration: 2000,
};

// pt-BR → 1.234.567,89
counterUp("#l-ptbr", { ...opts, locale: "pt-BR" });

// en-US → 1,234,567.89
counterUp("#l-enus", { ...opts, locale: "en-US" });

// de-DE → 1.234.567,89
counterUp("#l-dede", { ...opts, locale: "de-DE" });

// fr-FR → 1 234 567,89
counterUp("#l-frfr", { ...opts, locale: "fr-FR" });

Easing curva de animação

Três curvas embutidas: "linear", "easeInOutQuad", "easeOutCubic". Também aceita qualquer função (t: number) => number.

Comparação dos três easings embutidos
linear
0
easeInOutQuad
0
easeOutCubic
0
counterUp("#e-linear", {
  end: 1000,
  easing: "linear",
  duration: 2500,
});

counterUp("#e-inout", {
  end: 1000,
  easing: "easeInOutQuad",
  duration: 2500,
});

counterUp("#e-outcubic", {
  end: 1000,
  easing: "easeOutCubic", // padrão
  duration: 2500,
});
Easing personalizado — função custom (bounceOut)
Bounce out
0
function bounceOut(t) {
  const n1 = 7.5625, d1 = 2.75;
  if (t < 1 / d1) return n1 * t * t;
  if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
  if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
  return n1 * (t -= 2.625 / d1) * t + 0.984375;
}

counterUp("#e-bounce", {
  end: 1000,
  easing: bounceOut, // função diretamente
  duration: 2200,
});

Formatter formatação total

A opção formatter recebe (value, element, index) e deve retornar uma string. Substitui toda a lógica padrão de formatação.

Abreviação automática — K / M / B
Visualizações
0
Receita global
0
function abbrev(v) {
  if (v >= 1e9) return (v / 1e9).toFixed(1) + "B";
  if (v >= 1e6) return (v / 1e6).toFixed(1) + "M";
  if (v >= 1e3) return (v / 1e3).toFixed(1) + "K";
  return Math.floor(v).toString();
}

counterUp("#fmt-views", {
  end: 4700000,
  formatter: abbrev,
  duration: 2000,
});

counterUp("#fmt-revenue", {
  end: 1200000000,
  formatter: (v) => "R$ " + abbrev(v),
  duration: 2200,
});
Formato de tempo — MM:SS
Duração do vídeo
00:00
counterUp("#fmt-time", {
  end: 185, // 3 minutos e 5 segundos
  duration: 2000,
  formatter: (v) => {
    const s = Math.floor(v);
    const mm = String(Math.floor(s / 60)).padStart(2, "0");
    const ss = String(s % 60).padStart(2, "0");
    return `${mm}:${ss}`;
  },
});
Usando o parâmetro index em grupos
1º lugar
0
2º lugar
0
3º lugar
0
// formatter recebe (value, element, index)
// index = posição do elemento no grupo (0-based)
counterUp(".fmt-rank", {
  end: 1000,
  duration: 2000,
  formatter: (v, el, i) => {
    const medals = ["🥇", "🥈", "🥉"];
    return medals[i] + " " + Math.floor(v);
  },
});

Sleep / Stagger atraso escalonado

A opção sleep (ms) atrasa o início da animação. Ideal para escalonar múltiplos contadores sem setTimeout manual. É cancelado automaticamente por .stop(), .pause() ou .destroy().

Stagger — cada contador inicia depois do anterior
0ms
0
300ms
0
600ms
0
900ms
0
counterUp("#sg1", { end: 320,   sleep: 0   });
counterUp("#sg2", { end: 1450,  sleep: 300 });
counterUp("#sg3", { end: 9870,  sleep: 600 });
counterUp("#sg4", { end: 52400, sleep: 900 });

// sleep é cancelado automaticamente por:
// .stop() / .pause() / .destroy()
Getter .waiting — monitorar estado do sleep
Inicia em 2000ms
0
waiting
const c = counterUp("#sg-wait", {
  end: 500,
  sleep: 2000,   // aguarda 2 segundos
});

// c.waiting → true enquanto aguarda o sleep
// c.running → true quando a animação roda
const poll = setInterval(() => {
  if (c.waiting)      badge.textContent = "waiting";
  else if (c.running) badge.textContent = "running";
  else {
    badge.textContent = "concluído";
    clearInterval(poll);
  }
}, 100);

Viewport startOnView

Com startOnView: true, a animação aguarda o elemento entrar na área visível (usa IntersectionObserver). Role a página para ativar os exemplos abaixo.

Anima ao entrar na viewport — once: true (uma vez só)
Vendas
0
Clientes
0
counterUp("#vp1", {
  end: 75000,
  duration: 2000,
  startOnView: true, // aguarda entrar na tela
  once: true,        // anima apenas uma vez
  threshold: 0.2,    // dispara com 20% visível
  rootMargin: "0px",
});

counterUp("#vp2", {
  end: 12000,
  duration: 2000,
  startOnView: true,
  once: false,       // reinicia toda vez que aparecer
  threshold: 0.5,    // 50% visível para disparar
});
Opções avançadas do IntersectionObserver
Com rootMargin negativo
0
counterUp("#vp3", {
  end: 9999,
  duration: 1800,
  startOnView: true,
  once: true,
  root: null,                       // usa o viewport
  rootMargin: "0px 0px -80px 0px", // margem interna
  threshold: 0.1,
});

Múltiplos elementos group instance

Quando o seletor retorna mais de um elemento, a função retorna uma instância de grupo com a mesma API — aplicada a todos os elementos.

Mesmo valor para todos os elementos
Métrica A
0
Métrica B
0
Métrica C
0
const group = counterUp(".multi-same", {
  end: 9999,
  duration: 2000,
});

// Getters do grupo:
// group.count   → 3  (quantidade de elementos)
// group.values  → [v1, v2, v3]
// group.running → true se algum animar
// group.paused  → true se algum pausar
Valores diferentes por elemento — .update(array)
Janeiro
0
Fevereiro
0
Março
0
// Cria com autostart: false, depois usa update
const g = counterUp(".multi-diff", {
  autostart: false,
});

// update distribui um valor por elemento
g.update([1200, 980, 1540]);   // Trim 1
g.update([1750, 2100, 1900]);  // Trim 2
g.update([3200, 2850, 4100]);  // Trim 3

// Se o array tiver menos valores que elementos,
// o último é repetido para os restantes.
NodeList e Array de elementos como target
NodeList
0
Array
0
// NodeList direto
const nodes = document.querySelectorAll(".nodelist-el");
counterUp(nodes, { end: 777, duration: 1800 });

// Array de elementos DOM
const els = [document.querySelector(".array-el")];
counterUp(els, { end: 888, duration: 1800 });

Controles start / pause / resume / stop / reset / set

Controle total sobre o ciclo de vida da animação. Use autostart: false para aguardar uma chamada manual a .start().

Todos os métodos disponíveis
Score
0
parado
value: 0  |  running: false  |  paused: false
const c = counterUp("#ctrl-num", {
  end: 10000,
  duration: 5000,
  autostart: false, // aguarda .start() manual
});

// .start()  → inicia; se pausado, retoma
// .pause()  → pausa preservando progresso
// .resume() → continua do ponto de pausa
// .stop()   → para e reseta progresso interno
// .reset()  → para e volta display para start

// Getters em tempo real:
c.value   // número atual (sem formatação)
c.running // true se animando
c.paused  // true se pausado
c.waiting // true se aguardando sleep
.set(value) — definir valor instantaneamente
Valor direto (sem animação)
0
const c = counterUp("#set-num", {
  end: 1000,
  autostart: false,
});

// Define instantaneamente — para qualquer animação
c.set(100); // → exibe "100" imediatamente
c.set(500); // → exibe "500" imediatamente
c.set(999); // → exibe "999" imediatamente

// Anima do valor atual (999) até 2000
c.update(2000, { duration: 1200 });

Callbacks onUpdate / onComplete

onUpdate(value, element, index) é chamado a cada frame. onComplete(value, element, index) é chamado uma vez ao terminar. Em modo headless, element é null.

onUpdate + onComplete em tempo real
Contador
0
counterUp("#cb-num", {
  end: 500,
  duration: 2000,

  onUpdate: (value, element, index) => {
    // chamado a cada frame de animação (~60fps)
    log(`onUpdate → ${value.toFixed(1)}`);
  },

  onComplete: (value, element, index) => {
    // chamado uma vez ao terminar
    log(`✔ onComplete → ${value}`);
  },
});
Usando onUpdate para sincronizar outros elementos
Valor principal
0
Progresso: 0%
const MAX = 800;

counterUp("#cb-main", {
  end: MAX,
  duration: 2500,

  onUpdate: (value) => {
    // atualiza barra de progresso em sincronia
    const pct = (value / MAX * 100).toFixed(1);
    barEl.style.width = pct + "%";
    pctEl.textContent = pct + "%";
  },
});

Update .update()

.update(nextEnd, nextOptions?) muda o valor alvo e reinicia a animação a partir do valor atual. Ideal para dados em tempo real sem recriar a instância.

Preço em tempo real — anima do valor atual para o novo
Cotação
R$ 0,00
const ticker = counterUp("#upd-price", {
  end: 1200,
  duration: 800,
  prefix: "R$ ",
  decimals: 2,
});

// Anima do valor atual até o novo destino
btn_low.onclick  = () => ticker.update(1200);
btn_mid.onclick  = () => ticker.update(3450);
btn_high.onclick = () => ticker.update(9999);

// Aceita novas opções junto com o novo valor
btn_rand.onclick = () =>
  ticker.update(
    +(Math.random() * 10000).toFixed(2),
    { duration: 500 },
  );
Trocar formatação dinamicamente com update
Valor
0
const c = counterUp("#upd-fmt", {
  end: 1500,
  duration: 1000,
});

btnBRL.onclick = () =>
  c.update(1500, {
    prefix: "R$ ", suffix: "",
    decimals: 2, locale: "pt-BR",
  });

btnUSD.onclick = () =>
  c.update(1500, {
    prefix: "$ ", suffix: "",
    decimals: 2, locale: "en-US",
  });

btnPct.onclick = () =>
  c.update(75, {
    prefix: "", suffix: "%",
    decimals: 1, useGrouping: false,
  });

Auto-detecção end e decimals do HTML

Quando end é omitido e há um elemento DOM, a biblioteca lê o textContent e usa esse valor como destino. O decimals também é inferido automaticamente. Perfeito para conteúdo renderizado pelo servidor.

HTML já contém o valor — zero configuração extra
Inteiro
42500
Decimal (auto)
15.75
Grande
1234567
<!-- HTML renderizado pelo servidor (PHP, etc.) -->
<div class="auto-det">42500</div>
<div class="auto-det">15.75</div>
<div class="auto-det">1234567</div>

<script>
// Não precisa de `end` nem de `decimals` —
// cada elemento usa seu textContent como alvo.
counterUp(".auto-det", {
  duration: 2000,
});
</script>

Contagem regressiva start > end

Basta definir start maior que end. A animação desce automaticamente.

Countdown — bateria, estoque e cronômetro
Bateria
100%
Estoque
1000
Cronômetro
05:00
// start > end → conta de cima para baixo
counterUp("#cd-bat", {
  start: 100, end: 0,
  suffix: "%",
  duration: 3000,
  useGrouping: false,
});

counterUp("#cd-stock", {
  start: 1000, end: 37,
  duration: 2500,
});

// Cronômetro regressivo com formatter
counterUp("#cd-time", {
  start: 300, end: 0, // 300 s → 0
  duration: 3000,
  formatter: (v) => {
    const s = Math.ceil(v);
    return String(Math.floor(s / 60)).padStart(2, "0")
         + ":" + String(s % 60).padStart(2, "0");
  },
});

Modo Headless sem DOM

Passe null como target. A animação roda sem elemento DOM — os valores chegam exclusivamente pelo onUpdate. Funciona em Node.js, Next.js, Nuxt e Vitest sem jsdom.

Barras de progresso controladas por counters headless
Upload 0%
Download 0.0 MB
Processamento 0 / 1000
// target = null → modo headless (zero DOM)

counterUp(null, {
  start: 0, end: 100,
  duration: 3000,
  onUpdate: (v) => {
    pctEl.textContent  = Math.round(v) + "%";
    bar1.style.width   = v + "%";
  },
  onComplete: () => {
    pctEl.textContent = "100% ✓";
  },
});

counterUp(null, {
  start: 0, end: 512,
  duration: 4000,
  onUpdate: (v) => {
    mbEl.textContent = v.toFixed(1) + " MB";
    bar2.style.width = (v / 512 * 100) + "%";
  },
});

counterUp(null, {
  start: 0, end: 1000,
  duration: 3500,
  onUpdate: (v) => {
    const n = Math.floor(v);
    itemsEl.textContent = n + " / 1000";
    bar3.style.width    = (v / 10) + "%";
  },
});
Simulação de saída Node.js / SSR
Equivalente no Node.js
// Node.js — importe o ESM:
// import { counterUp } from "@nullsablex/counter-up";

counterUp(null, {
  start: 0,
  end: 1000,
  duration: 3000,
  onUpdate: (value) => {
    process.stdout.write(`\r${Math.round(value)}`);
  },
  onComplete: (value) => {
    console.log(`\nFim: ${value}`);
  },
});

// Compatível com:
// ✓ Node.js puro
// ✓ Next.js (getServerSideProps)
// ✓ Nuxt (server-side)
// ✓ Vitest sem jsdom
// ✓ Qualquer ambiente sem window