Básico padrão
Uso mínimo: passe o seletor e o valor final. Todos os outros parâmetros são opcionais.
counterUp("#b1", {
end: 48230,
duration: 2000,
});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
});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.
counterUp("#f-currency", {
start: 0,
end: 128750.90,
duration: 2200,
decimals: 2,
prefix: "R$ ",
locale: "pt-BR",
});counterUp("#f-pct1", {
end: 97,
suffix: "%",
duration: 1600,
useGrouping: false,
});
counterUp("#f-pct2", {
end: 99.98,
suffix: "%",
decimals: 2,
duration: 1800,
useGrouping: false,
});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,
});useGroupingcounterUp("#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.
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.
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,
});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.
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,
});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}`;
},
});index em grupos// 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().
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().waiting — monitorar estado do sleepconst 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.
once: true (uma vez só)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
});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.
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.update(array)// 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 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().
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 instantaneamenteconst 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.
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}`);
},
});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.
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 },
);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 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.
// 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.
// 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) + "%";
},
});// 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