gsferro/filament-odometer-easy
| Install | |
|---|---|
composer require gsferro/filament-odometer-easy |
|
| Latest Version: | v1.2.0 |
| PHP: | ^8.2 |
| License: | MIT |
| Last Updated: | Aug 1, 2026 |
| Links: | GitHub · Packagist |
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.
Related Packages
Stat cards with a corner icon and a colored accent border for Filament v3, v4 an...
Matomo Analytics integration for Filament Panels with a set of widgets to displa...
A thumb-friendly mobile bottom navigation bar for Filament panels. It programmat...
Filament StateFusion is a powerful FilamentPHP plugin that seamlessly integrates...
A comprehensive Laravel Filament 3 💡 starter kit with pre-installed plugins, ad...

