Contador
Animação de número: conta até o valor final quando entra na tela, com desaceleração no fim. É um texto inline que herda o estilo de onde é usado (títulos, cards, frases, Números Principais). Leitores de tela recebem o valor final de uma vez e, com “reduzir movimento”, ele aparece direto.
Playground
Props
Geradas dos tipos TypeScript do componente; a descrição vem dos comentários /** */.
<Contador>
Número que conta de `de` até `valor` quando entra na tela e depois fica parado. É só a animação: um texto inline que herda fonte, tamanho e cor de onde for usado (um título, um card, uma frase). O layout fica com quem o usa. O número animado é só visual (`aria-hidden`): leitores de tela recebem o valor final de uma vez, num texto escondido. Uma cópia invisível do valor final reserva a largura, então o texto em volta não pula durante a contagem. Com "reduzir movimento" o valor final aparece direto.
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
| valorobrigatória | number | — | Valor final da contagem. |
| de | number | 0 | Valor de onde a contagem parte. |
| duracao | number | 1400 | Duração (ms) da contagem. |
| casas | number | 0 | Casas decimais mostradas durante e no fim da contagem. |
| prefixo | string | "" | Texto antes do número ("R$ "). |
| sufixo | string | "" | Texto depois do número ("+", "%", " mil"). |
| formatar | (n: number) => string | — | Formata o número (já arredondado em `casas`). Padrão: separadores do pt-BR. |
| rotuloAcessivel | string | — | O que os leitores de tela leem no lugar do número. Padrão: o valor final com prefixo e sufixo. |
| className | string | — | Classes extras do número. Sem elas, herda o estilo do texto em volta. |
Código
stories.tsx tem os exemplos usados no preview e no playground.
"use client";
import { useEffect, useRef, useState } from "react";
import { useEmTela, useSemMovimento } from "@/library/revelar/Movimento";
/** Saída suave (como o "power2.out"): rápida no começo, devagar perto do valor final. */
const suavizar = (t: number) => 1 - (1 - t) ** 2;
/**
* Número que conta de `de` até `valor` quando entra na tela e depois fica parado.
* É só a animação: um texto inline que herda fonte, tamanho e cor de onde for
* usado (um título, um card, uma frase). O layout fica com quem o usa.
*
* O número animado é só visual (`aria-hidden`): leitores de tela recebem o valor
* final de uma vez, num texto escondido. Uma cópia invisível do valor final
* reserva a largura, então o texto em volta não pula durante a contagem. Com
* "reduzir movimento" o valor final aparece direto.
*/
export function Contador({
valor,
de = 0,
duracao = 1400,
casas = 0,
prefixo = "",
sufixo = "",
formatar,
rotuloAcessivel,
className,
}: {
/** Valor final da contagem. */
valor: number;
/** Valor de onde a contagem parte. */
de?: number;
/** Duração (ms) da contagem. */
duracao?: number;
/** Casas decimais mostradas durante e no fim da contagem. */
casas?: number;
/** Texto antes do número ("R$ "). */
prefixo?: string;
/** Texto depois do número ("+", "%", " mil"). */
sufixo?: string;
/** Formata o número (já arredondado em `casas`). Padrão: separadores do pt-BR. */
formatar?: (n: number) => string;
/** O que os leitores de tela leem no lugar do número. Padrão: o valor final com prefixo e sufixo. */
rotuloAcessivel?: string;
/** Classes extras do número. Sem elas, herda o estilo do texto em volta. */
className?: string;
}) {
const ref = useRef<HTMLSpanElement>(null);
const semMovimento = useSemMovimento();
const emTela = useEmTela(ref, { margem: "0px 0px -15% 0px" });
const [atual, setAtual] = useState(de);
useEffect(() => {
if (!emTela || semMovimento) return;
let quadro = 0;
const inicio = performance.now();
const passo = (agora: number) => {
const t = duracao > 0 ? Math.min((agora - inicio) / duracao, 1) : 1;
setAtual(de + (valor - de) * suavizar(t));
if (t < 1) quadro = requestAnimationFrame(passo);
};
quadro = requestAnimationFrame(passo);
return () => cancelAnimationFrame(quadro);
}, [emTela, semMovimento, de, valor, duracao]);
const escrever = (n: number) => {
const arredondado = Number(n.toFixed(casas));
const numero = formatar
? formatar(arredondado)
: arredondado.toLocaleString("pt-BR", { minimumFractionDigits: casas, maximumFractionDigits: casas });
return `${prefixo}${numero}${sufixo}`;
};
const final = escrever(valor);
const mostrado = semMovimento ? final : escrever(atual);
return (
<span ref={ref} className={className}>
{/* Valor final invisível na mesma célula da grade: guarda a largura máxima. */}
<span aria-hidden="true" className="inline-grid tabular-nums">
<span className="invisible col-start-1 row-start-1">{final}</span>
<span className="col-start-1 row-start-1">{mostrado}</span>
</span>
<span className="sr-only">{rotuloAcessivel ?? final}</span>
</span>
);
}
import { useState } from "react";
import { defineStories } from "@/registry/stories";
import { NumerosPrincipais } from "@/library/numeros-principais/NumerosPrincipais";
import { Contador } from "./Contador";
type Valores = {
valor: number;
de: number;
duracao: number;
casas: number;
prefixo: string;
sufixo: string;
};
/** Playground com um botão para ver a contagem de novo (a chave remonta o contador). */
function ExemploComRepetir(a: Valores) {
const [vez, setVez] = useState(0);
return (
<div className="flex flex-col items-center gap-6 text-center">
<span className="font-display text-6xl leading-none font-extrabold tracking-[-0.03em] text-accent">
<Contador key={vez} valor={a.valor} de={a.de} duracao={a.duracao} casas={a.casas} prefixo={a.prefixo} sufixo={a.sufixo} />
</span>
<button
type="button"
onClick={() => setVez((v) => v + 1)}
className="rounded-full border border-rule-strong px-4 py-2 text-sm font-medium text-ink outline-accent hover:bg-surface focus-visible:outline-2 focus-visible:outline-offset-2"
>
Contar de novo
</button>
</div>
);
}
export default defineStories({
args: { valor: 25, de: 0, duracao: 1400, casas: 0, prefixo: "", sufixo: "+" },
controls: {
valor: { type: "number", step: 1 },
de: { type: "number", step: 1 },
duracao: { type: "number", label: "duracao (ms)", min: 0, max: 5000, step: 100 },
casas: { type: "number", label: "casas decimais", min: 0, max: 3 },
prefixo: { type: "text" },
sufixo: { type: "text" },
},
render: (a) => <ExemploComRepetir {...a} />,
stories: [
{ name: "Padrão" },
{
name: "Dentro de uma frase",
render: () => (
<p className="max-w-md text-lg leading-relaxed text-ink-2">
Já atendemos <Contador valor={1280} className="font-semibold text-ink" /> clientes, com{" "}
<Contador valor={98.6} casas={1} sufixo="%" className="font-semibold text-ink" /> dos pedidos entregues no prazo.
</p>
),
},
{
name: "Em Números Principais",
render: () => (
<div className="w-full">
<NumerosPrincipais
itens={[
{ valor: <Contador valor={48} prefixo="R$ " sufixo=" mil" />, texto: "de receita em outubro." },
{ valor: <Contador valor={1284} />, texto: "pedidos pagos no mês." },
{ valor: <Contador valor={3.2} casas={1} sufixo="%" />, texto: "dos visitantes compraram algo." },
]}
/>
</div>
),
},
{ name: "Contagem regressiva", args: { valor: 0, de: 100, duracao: 2000, sufixo: "%" } },
],
});
Para usar em outro projeto, copie também
Os componentes de que este depende e o tema. O CSS do tema é um arquivo só: importe depois do Tailwind.
Componentes da biblioteca (diretos e indiretos)
Tema escolhido no cabeçalho (com a ponte para o Tailwind)
Ver o CSS do tema
/* Tema "Editorial", gerado no Component Vault.
Importe depois de @import "tailwindcss". Carregue as fontes "Archivo" e "Inter" no projeto. */
@theme inline {
--color-page: var(--page);
--color-surface: var(--surface);
--color-track: var(--track);
--color-grid: var(--grid);
--color-ink: var(--ink);
--color-ink-2: var(--ink-2);
--color-muted: var(--muted);
--color-rule: var(--rule);
--color-rule-strong: var(--rule-strong);
--color-axis: var(--axis);
--color-accent: var(--accent);
--color-success: var(--success);
--color-warning: var(--warning);
--color-critical: var(--critical);
--font-display: var(--fonte-titulo);
--shadow-flutuante: var(--sombra-flutuante);
}
:root {
color-scheme: light dark;
--page: light-dark(#ffffff, #0f0f0e);
--surface: light-dark(#f5f5f5, #1a1a19);
--track: light-dark(#f0f0f0, #262624);
--grid: light-dark(#ededed, #232321);
--ink: light-dark(#111111, #f0efec);
--ink-2: light-dark(#4a4a4a, #c3c2b7);
--muted: light-dark(#666666, #a3a29a);
--rule: light-dark(#e3e3e3, #2c2c2a);
--rule-strong: light-dark(#111111, #f0efec);
--axis: light-dark(#9a9a9a, #6b6a64);
--accent: light-dark(#1a56db, #7aa2f7);
--success: light-dark(#13865c, #3fc48f);
--warning: #fab219;
--critical: #d03b3b;
--series-1: light-dark(#2a78d6, #3987e5);
--series-2: light-dark(#eb6834, #d95926);
--series-3: light-dark(#1baf7a, #199e70);
--series-4: light-dark(#eda100, #c98500);
--series-5: light-dark(#e87ba4, #d55181);
--series-6: #008300;
--series-7: light-dark(#6250d6, #9085e9);
--series-8: light-dark(#e34948, #e66767);
--fonte-titulo: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
--fonte-texto: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
--spacing: 0.25rem;
--text-xs: 0.8rem;
--text-sm: 0.8944rem;
--text-base: 1rem;
--text-lg: 1.118rem;
--text-xl: 1.25rem;
--text-2xl: 1.5625rem;
--text-3xl: 1.9531rem;
--text-4xl: 2.4414rem;
--text-5xl: 3.0518rem;
--text-6xl: 3.8147rem;
--text-7xl: 4.7684rem;
--radius-xs: 2px;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-2xl: 16px;
--radius-3xl: 24px;
--radius-4xl: 32px;
--sombra-flutuante: 0 8px 24px -6px rgb(0 0 0 / 0.14), 0 2px 6px -2px rgb(0 0 0 / 0.08);
color: var(--ink);
font-family: var(--fonte-texto);
}
Na vitrine, a ponte com o Tailwind fica em src/themes/tokens.css:
/* Ponte entre os tokens do tema e o Tailwind.
Os componentes da biblioteca usam classes comuns do Tailwind (p-5, text-sm,
rounded-xl...) e as cores semânticas abaixo (bg-page, text-ink...). Dentro de
[data-theme="vault"], o tema redefine as variáveis que essas classes leem:
--spacing, --text-*, --radius-* e as cores. Os valores vêm do tema escolhido
na vitrine (src/themes/presets.ts) e são gerados por themeCss(). */
@theme inline {
--color-page: var(--page);
--color-surface: var(--surface);
--color-ink: var(--ink);
--color-ink-2: var(--ink-2);
--color-muted: var(--muted);
--color-rule: var(--rule);
--color-rule-strong: var(--rule-strong);
--color-track: var(--track);
--color-grid: var(--grid);
--color-axis: var(--axis);
--color-accent: var(--accent);
--color-success: var(--success);
--color-warning: var(--warning);
--color-critical: var(--critical);
--font-display: var(--fonte-titulo);
--shadow-flutuante: var(--sombra-flutuante);
}