Filament Odometer Easy
Animated counters for Filament v3, v4 and v5 — tables, infolists, stats and navigation badges — powered by number-flow (default) or odometer.js.
#Filament Odometer Easy
🇧🇷 Português · 🇺🇸 English
Contadores animados para o Filament v3, v4 e v5 — tabelas, infolists e widgets de estatísticas — do jeito mais simples possível: instale, registre o plugin e use.
É o mesmo efeito do contador "Items found" da página oficial filamentphp.com/plugins, pronto para os seus dashboards e métricas em tempo real.
#🎬 Demo
OdometerStat no dashboard — com poll, os contadores re-animam sozinhos a cada atualização de valor:
OdometerColumn em tabelas — animação no load, na ordenação e na troca de página:
OdometerEntry em infolists e OdometerNavigationBadge em menus:
Badge visível com a sidebar recolhida — o Filament esconde o badge quando o menu recolhe; com ->badgeOnCollapsedSidebar() ele passa a flutuar no canto do ícone, no mesmo formato do botão de filtros da tabela:
| Claro | Escuro |
|---|---|
#Componentes
| Componente | Estende | Uso |
|---|---|---|
OdometerColumn |
TextColumn |
Colunas de tabela |
OdometerEntry |
TextEntry |
Entries de infolist |
OdometerStat |
Stat |
Counts no StatsOverviewWidget |
OdometerNavigationBadge |
— | Badge de navegação (getNavigationBadge()) — com opção de continuar visível com a sidebar recolhida |
Facade FilamentOdometerEasy |
— | Qualquer view/blade customizado |
Todos herdam 100% da API do componente base (sortable, searchable, label, description, color etc.) — só o valor passa a ser animado.
#Motores de animação (drivers)
O pacote traz dois motores e você escolhe por config ou de forma fluente no plugin:
#number-flow — padrão ⭐
O web component number-flow (usado pelo próprio site do Filament):
- ✅ Zero dependências — sem jQuery, sem CDN; o bundle (~16 KB) já vem no pacote
- ✅ Anima do 0 no primeiro render — exibe 0 e, após um delay configurável, anima até o valor
- ✅ Re-anima a cada atualização — perfeito com Livewire,
poll()e dashboards em tempo real - ✅ Formatação nativa via
Intl.NumberFormat— moeda, decimais e locale (pt-BR→1.000,00) - ✅ Acessível — respeita
prefers-reduced-motion - ✅ Mantido ativamente
#odometer — secundário
O efeito clássico do odometer.js via gsferro/laravel-odometer-easy (instalado como dependência):
- 🎨 7 temas visuais:
default,car,digital,minimal,plaza,slot-machine,train-station - ⚠️ Depende do jQuery (o plugin injeta automaticamente no
<head>dos painéis) - ⚠️ Anima apenas na primeira renderização (não re-anima ao atualizar o valor)
#Compatibilidade
| Filament | Suporte | Observações |
|---|---|---|
| 5.x | ✅ | |
| 4.x | ✅ | |
| 3.x (3.2+) | ✅ |
A mesma versão do pacote atende as três — o Composer resolve pela versão do Filament do seu projeto. Requer PHP 8.2+.
#Instalação
composer require gsferro/filament-odometer-easy php artisan filament:assets
Registre o plugin no seu painel:
use Gsferro\FilamentOdometerEasy\FilamentOdometerEasyPlugin; public function panel(Panel $panel): Panel { return $panel // ... ->plugin(FilamentOdometerEasyPlugin::make()); }
Pronto. ✨ Sem npm, sem publicar views, sem configurar assets — o driver number-flow já funciona.
[!TIP] A maioria das apps já roda
filament:assetsautomaticamente nopost-autoload-dump(viafilament:upgrade). Nesse caso, basta ocomposer require.
#Uso
#Coluna de tabela
use Gsferro\FilamentOdometerEasy\Tables\Columns\OdometerColumn; OdometerColumn::make('total_vendas') ->label('Total de vendas') ->sortable(),
#Entry de infolist
use Gsferro\FilamentOdometerEasy\Infolists\Components\OdometerEntry; OdometerEntry::make('total_vendas') ->label('Total de vendas'),
#Stat (StatsOverviewWidget)
use Gsferro\FilamentOdometerEasy\Widgets\OdometerStat; protected function getStats(): array { return [ OdometerStat::make('Total de vendas', Venda::count()) ->description('Últimos 30 dias') ->descriptionIcon('heroicon-m-arrow-trending-up') ->color('success'), ]; }
[!TIP] Combine com
->poll('10s')no widget: com o drivernumber-flow, o contador re-anima a cada atualização de valor. 📈
#Badge de navegação (menu do painel)
use Gsferro\FilamentOdometerEasy\Navigation\OdometerNavigationBadge; // no Resource (ou Page) public static function getNavigationBadge(): ?string { return OdometerNavigationBadge::make(static::getModel()::count()); } // ou em um NavigationItem customizado NavigationItem::make('Vendas') ->badge(fn (): string => OdometerNavigationBadge::make(Venda::count())),
A API de navegação do Filament só aceita string (HTML é escapado), então o
componente envolve o valor com um marcador invisível e o JS do pacote troca o
texto do badge por um <number-flow> animado. A formatação usa a config global
do number-flow (locales, format, delay, duration).
[!NOTE] Disponível apenas no driver
number-flow. No driverodometer, o valor é exibido como texto puro, sem animação.
#Mantendo o badge visível com a sidebar recolhida
Com ->sidebarCollapsibleOnDesktop() no painel, o Filament esconde o badge assim que a
sidebar recolhe: o container carrega x-show="$store.sidebar.isOpen" e ganha display:none
inline. A contagem some justamente no modo em que só há ícone — o modo com menos informação.
A opção vive no plugin, dentro do seu Panel Provider — e depende de o painel ter a sidebar recolhível, que é o estado que ela cobre:
// app/Providers/Filament/AdminPanelProvider.php public function panel(Panel $panel): Panel { return $panel ->id('admin') ->path('admin') // 1. pré-requisito: sem sidebar recolhível não existe o estado a corrigir ->sidebarCollapsibleOnDesktop() ->plugin( FilamentOdometerEasyPlugin::make() // 2. mantém o badge visível quando ela recolhe ->badgeOnCollapsedSidebar() ); }
[!WARNING] Sem
->sidebarCollapsibleOnDesktop()(ou->sidebarFullyCollapsibleOnDesktop()) no painel, a opção não faz nada: o Filament nunca entra no estado recolhido, e o CSS só age em.fi-main-sidebar:not(.fi-sidebar-open).
O badge passa a flutuar no canto superior direito do ícone, com fundo sólido recortando a borda — exatamente o formato que o Filament já usa no gatilho de filtros da tabela. Com a sidebar aberta, nada muda: o layout nativo (badge em linha, à direita do rótulo) é preservado.
- ✅ Só CSS — nenhuma view do Filament publicada, nenhum JavaScript
- ✅ Inline no
<head>(~600 bytes) — não exigephp artisan filament:assets - ✅ Vale para os dois drivers: é posicionamento do badge do Filament, não do contador
- ✅ Modo claro e escuro, e RTL
[!IMPORTANT] Opt-in. Fica desligado por padrão para que atualizar a versão não mude a aparência do menu de quem não pediu. Para ligar via config:
'badge-on-collapsed-sidebar' => true.
[!TIP] A folga à direita do item é de ~16px, então contagens de 5+ dígitos podem perder 1-2px na borda (
.fi-sidebar-navéoverflow-x:hidden). Se for o seu caso, use notação compacta:->format(['notation' => 'compact'])— 12.345 vira12K.
#Em qualquer view (facade)
use Gsferro\FilamentOdometerEasy\Facades\FilamentOdometerEasy; // driver configurado (number-flow por padrão) FilamentOdometerEasy::render(1500); // forçando um driver pontualmente FilamentOdometerEasy::renderNumberFlow(1500, format: ['style' => 'currency', 'currency' => 'BRL']); FilamentOdometerEasy::renderOdometer(1500, format: '(.ddd),dd', class: 'h3');
#Formatação
O método ->format() está disponível em todos os componentes e aceita o formato do driver ativo. No driver number-flow (padrão), passe um array com opções do Intl.NumberFormat — a formatação (símbolo, separadores, casas decimais) é aplicada pelo navegador, animada dígito a dígito.
#Moeda (R$, US$, €…)
Por padrão o contador exibe apenas o número. Para mostrar o símbolo da moeda, passe um format com style: currency:
OdometerStat::make('Valor aprovado (projetos em andamento)', $aprovado) ->format(['style' => 'currency', 'currency' => 'BRL']),
[!TIP] Combine com
->locales('pt-BR')no plugin (ou na config) para obterR$ 1.234,56— sem locale, o navegador do usuário decide os separadores.
#Receitas prontas (driver number-flow)
| Resultado (pt-BR) | ->format([...]) |
|---|---|
R$ 1.234,56 (moeda) |
['style' => 'currency', 'currency' => 'BRL'] |
R$ 1.235 (moeda sem centavos) |
['style' => 'currency', 'currency' => 'BRL', 'maximumFractionDigits' => 0] |
US$ 1.234,56 / € 1.234,56 |
['style' => 'currency', 'currency' => 'USD'] / 'EUR' |
12,5% (percentual) |
['style' => 'percent', 'minimumFractionDigits' => 1] |
1.234,50 (decimais fixos) |
['minimumFractionDigits' => 2, 'maximumFractionDigits' => 2] |
1,2 mi (notação compacta) |
['notation' => 'compact'] |
1.234 km (unidades) |
['style' => 'unit', 'unit' => 'kilometer'] |
+1.234 (sinal sempre visível) |
['signDisplay' => 'always'] |
1234 (sem agrupamento) |
['useGrouping' => false] |
[!WARNING]
style: percentmultiplica o valor por 100 — passe0.125para exibir12,5%.
#Formato dinâmico (Closure)
->format() também aceita Closure. Em colunas e entries, o Filament injeta $record/$state:
OdometerColumn::make('saldo') ->format(fn (Conta $record): array => [ 'style' => 'currency', 'currency' => $record->moeda, // BRL, USD, EUR... ]),
#Velocidade da animação
Todos os componentes aceitam ->duration() (driver number-flow; quanto maior, mais lento):
OdometerStat::make('Receita', $total) ->duration(2000), // conta em câmera lenta ✨
#Driver odometer
No driver secundário, ->format() recebe a string data-format do odometer.js:
OdometerColumn::make('receita') ->format('(.ddd),dd'),
#Onde configurar cada opção do number-flow
| Opção | Por componente | Global (plugin/config) | O que faz |
|---|---|---|---|
format |
->format([...]) |
->format([...]) |
Opções do Intl.NumberFormat (moeda, percentual, decimais…) |
duration |
->duration(ms) |
->duration(ms) |
Velocidade da animação (padrão ~900ms) |
locales |
— | ->locales('pt-BR') |
Idioma/separadores (1.000,00) |
delay |
— | ->delay(ms) |
Espera antes da animação inicial 0 → valor (padrão 500ms) |
O valor por componente sempre vence o global. A facade FilamentOdometerEasy::renderNumberFlow() aceita todas as opções por chamada (format, delay, duration).
Referências: opções do Intl.NumberFormat · format do odometer.
#Configuração
#Fluente, direto no plugin
FilamentOdometerEasyPlugin::make() ->locales('pt-BR') // number-flow: 1.000,00 ->format(['style' => 'currency', 'currency' => 'BRL']) // padrão global ->delay(500) // ms antes da animação inicial (0 → valor) ->duration(1500) // velocidade da animação em ms (padrão ~900ms) ->badgeOnCollapsedSidebar(), // badge do menu visível com a sidebar recolhida
Para usar o motor clássico:
FilamentOdometerEasyPlugin::make() ->driver('odometer') ->theme('digital') // default, car, digital, minimal, plaza, slot-machine, train-station ->format('(.ddd),dd') // data-format padrão ->jquery(enabled: false), // quando a aplicação já carrega o jQuery
#Ou pelo arquivo de config
php artisan vendor:publish --tag="filament-odometer-easy-config"
return [ // number-flow (padrão) | odometer 'driver' => 'number-flow', // mantém o navigation badge visível com a sidebar recolhida no desktop 'badge-on-collapsed-sidebar' => false, 'number-flow' => [ 'locales' => null, // ex.: 'pt-BR'; null usa o locale do navegador 'format' => null, // ex.: ['style' => 'currency', 'currency' => 'BRL'] 'delay' => 500, // ms antes da animação inicial: exibe 0 e anima até o valor 'duration' => null, // velocidade da animação em ms; null usa o padrão (~900ms) ], 'odometer' => [ 'theme' => 'default', 'format' => null, // ex.: '(.ddd),dd'; null usa o padrão (pt-BR: 1.000,00) 'jquery' => [ 'enabled' => true, 'src' => 'https://code.jquery.com/jquery-4.0.0.min.js', 'integrity' => 'sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=', ], ], ];
#Como funciona por baixo dos panos
- number-flow: o pacote já entrega o web component
<number-flow>bundlado (resources/dist/filament-odometer-easy.js, registrado como ES module viaFilamentAsset), o mesmo usado em filamentphp.com/plugins. A view Blade renderiza o elemento comdata-value/data-format/data-localese o bundle o inicializa: exibe 0, espera odelaye anima até o valor. UmMutationObserveracompanha as mudanças dedata-valuefeitas pelo morph do Livewire (poll, refresh) e re-anima do valor atual para o novo — sem depender dex-init, que não roda de novo quando o Livewire preserva o elemento. - Navigation badge:
getNavigationBadge()eNavigationItem::badge()são tipados como?stringe o Blade escapa o conteúdo, então não dá para retornar HTML.OdometerNavigationBadge::make()envolve o valor comU+2060(word joiner, invisível); o bundle detecta o marcador no.fi-badge-label, troca o texto por um<number-flow>e usa a config global exposta emwindow.filamentOdometerEasypor render hook. Quando o Livewire re-renderiza o badge, a animação parte do valor anterior (data-start). - Badge com a sidebar recolhida: não há prop, config nem render hook por item no Filament
para isso, e publicar a view
sidebar.itemcongelaria 150 linhas de Blade a cada upgrade. O pacote injeta um<style>no<head>por render hook, escopado em.fi-main-sidebar:not(.fi-sidebar-open)— o próprio Filament já expõe o estado da sidebar como classe (fi-sidebar-open) e o item de menu já éposition: relative. Odisplay: flex !importanté o que vence a declaração inline que ox-showdo Alpine escreve. Só dentro de@media (width >= 64rem), o mesmo breakpoint do store do Alpine do Filament. - odometer: os assets (tema css,
odometer.js,odometer-easy.js) são servidos direto do vendor dogsferro/odometer-easyviaFilamentAsset, e o jQuery é injetado por render hook no<head>dos painéis. - A troca de driver seleciona quais assets são registrados — nunca os dois ao mesmo tempo.
#Desenvolvimento
O bundle do number-flow só precisa ser regerado se você alterar resources/js/index.js:
npm install npm run build
#Testes
composer test
#Veja também
gsferro/filament-stat-plus-easy — cards de
stat com ícone no canto e borda de acento colorida, para Filament v3, v4 e v5. O StatPlus estende o
OdometerStat deste pacote, então o contador animado vem junto — mais o skeleton de carregamento
combinando.
#Changelog
Please see CHANGELOG for more information on what has changed recently.
#Contributing
Please see CONTRIBUTING for details.
#Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
#Credits
- gsferro
- number-flow by Maxwell Barvian
- odometer.js by HubSpot
- All Contributors
#License
The MIT License (MIT). Please see License File for more information.