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.

Maintainers

Package info

github.com/gsferro/filament-odometer-easy

pkg:composer/gsferro/filament-odometer-easy

Transparency log

Fund package maintenance!

gsferro

Statistics

Installs: 333

Dependents: 1

Suggesters: 0

Stars: 1

Open Issues: 0

v1.2.0 2026-08-01 22:53 UTC

This package is auto-updated.

Last update: 2026-08-01 23:52:42 UTC


README

filament-odometer-easy

Latest Version Total Downloads License

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:

OdometerStat com polling

OdometerColumn em tabelas — animação no load, na ordenação e na troca de página:

OdometerColumn em tabela

OdometerEntry em infolists e OdometerNavigationBadge em menus:

OdometerEntry em infolist e navigation badge

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
Badge na sidebar recolhida Badge na sidebar recolhida, modo 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-BR1.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 exige php 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 via FilamentAsset), o mesmo usado em filamentphp.com/plugins. A view Blade renderiza o elemento com data-value/data-format/data-locales e o bundle o inicializa: exibe 0, espera o delay e anima até o valor. Um MutationObserver acompanha as mudanças de data-value feitas pelo morph do Livewire (poll, refresh) e re-anima do valor atual para o novo — sem depender de x-init, que não roda de novo quando o Livewire preserva o elemento.
  • Navigation badge: getNavigationBadge() e NavigationItem::badge() são tipados como ?string e o Blade escapa o conteúdo, então não dá para retornar HTML. OdometerNavigationBadge::make() envolve o valor com U+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 em window.filamentOdometerEasy por 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.item congelaria 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. O display: flex !important é o que vence a declaração inline que o x-show do 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 do gsferro/odometer-easy via FilamentAsset, 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

License

The MIT License (MIT). Please see License File for more information.