gsferro / 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.
Fund package maintenance!
Requires
- php: ^8.2
- filament/filament: ^3.2|^4.0|^5.0
- gsferro/odometer-easy: ^1.0
- spatie/laravel-package-tools: ^1.15.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- nunomaduro/collision: ^8.0
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.7|^4.0
- pestphp/pest-plugin-arch: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- pestphp/pest-plugin-livewire: ^3.0|^4.0
README
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:assets automaticamente no post-autoload-dump
(via filament:upgrade). Nesse caso, basta o composer 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 driver number-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 driver odometer,
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 vira 12K.
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 obter
R$ 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: percent multiplica o valor por 100 — passe 0.125 para exibir 12,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.





