gsferro/odometer-easy
A Laravel package that simplifies adding animated odometer counters to Blade views, with easy configuration for values, formatting, and front-end integration.
Install the package with Composer:
composer require gsferro/odometer-easy
This package is a thin wrapper around HubSpot's Odometer jQuery plugin, so expect it to publish the plugin's CSS/JS assets and expose a Blade-friendly way to render animated number counters.
Where to look first
config/odometer.php (after php artisan vendor:publish --tag=odometer-config) — check defaults for themes, format, and asset paths.src/ directory — locate the ServiceProvider and any Blade component/directive classes. That's your real API surface.public/vendor/odometer-easy/ — confirm the JS/CSS actually landed there before assuming a Blade directive will work.First use case
Publish assets and a config:
php artisan vendor:publish --provider="Gsferro\OdometerEasy\OdometerEasyServiceProvider"
Then render a counter in a Blade view — depending on the version you may have a directive, a component, or a helper. The most likely shapes are:
{{-- directive style --}}
@odometer($totalUsers)
{{-- component style --}}
<x-odometer :value="$totalUsers" />
If neither works, read the provider's boot() method — small packages like this one evolve quickly and the README may lag behind the shipped API. The underlying Odometer call you're ultimately generating is:
new Odometer({ el: element, value: 1234, format: '(,ddd)', theme: 'default' });
Dashboard / KPI counters
The classic use case: a card that shows total revenue, users, orders. Combine with a Blade component:
<div class="stat">
<span class="stat-value">
@odometer($stats['revenue'], ['format' => '(,ddd).dd', 'theme' => 'car'])
</span>
<span class="stat-label">Revenue</span>
</div>
Updating counters after a fetch
Odometer animates transitions when you mutate innerHTML on an existing element. The pattern that works with any Odometer wrapper is to keep a reference to the DOM node and set its value after an AJAX call:
fetch('/api/stats')
.then(r => r.json())
.then(data => {
document.querySelector('#revenue').innerHTML = data.revenue;
// Odometer detects the write and animates from old -> new value
});
If the package exposes a JS helper (e.g. window.OdometerEasy.set(el, value)), prefer that over raw innerHTML so it can re-apply format/theme correctly.
Blade + Livewire
Because Livewire swaps DOM nodes, Odometer instances can be lost on re-render. Two options:
wire:ignore around the counter and push updates via $dispatch or a small Alpine component that calls new Odometer(...) on x-init.message.processed hook and reinitialize Odometer on the affected element.Formats and themes
Odometer's built-in formats are the ones worth memorizing:
| Format | Example output |
|---|---|
(,ddd) |
1,234,567 |
(,ddd).dd |
1,234,567.89 |
(.ddd),dd |
1.234.567,89 |
( ddd),dd |
1 234 567,89 |
Themes: default, car, digital, plaza, slot-machine, train-station. For an "easy" package, most of these are likely pass-through config in the directive's options array.
Integration tips
@push('scripts') / @push('styles') rather than loading them globally. The plugin is small but there's no reason to ship it on marketing pages.aria-label containing the static value, since screen readers can't track the animation.Asset publishing gotcha
Many small packages forget to include public/vendor/... in their publishes() call, or only publish on vendor:publish --force. If you get a 404 on odometer.min.js, check the provider's publishes() array and re-run with --force. You can also point a <script src> directly at the CDN as a workaround (https://cdnjs.cloudflare.com/ajax/libs/odometer/0.4.8/odometer.min.js).
jQuery dependency
Odometer depends on jQuery (or a jQuery-compatible shim). If your app runs Vite + Alpine and no jQuery, you'll need to either:
This is the single most common failure mode — the counter silently stays at its initial value because $ is undefined.
Re-initialization is cheap, mutation resets are not
Calling odometer.innerHTML = x on an existing instance animates. Calling element.innerHTML = x where the element is a new node (e.g. after a Livewire re-render) doesn't. If numbers "jump" instead of animating, you're working against a fresh node — reinitialize.
Format strings are literal
'(,ddd)' is not a number_format equivalent. Passing a PHP number_format() string won't translate; you have to emit the Odometer format token. If the package has a config key for format, set it once globally rather than per-call — the plugin only supports one format per instance.
Value types
Odometer expects a number or numeric string. Passing a PHP decimal cast that serializes to a string with trailing zeros is fine, but passing a formatted string with commas will break the animation (it parses with parseFloat). Keep the raw value in the HTML/data attribute and let the format option do the display work.
Debugging checklist
<span class="odometer-digit"> children? If yes, the plugin ran; the value is the problem. If no, initialization never fired.duration option defaults to 2000ms. If you set a value and see nothing for a moment, wait — it's animating.Extension points
Because the package is thin, the practical extension route is: use it to render the markup, then take over initialization yourself with vanilla new Odometer(...). That lets you control duration, theme, and format without waiting for the wrapper to expose options. Keep the package for the Blade ergonomics and asset publishing; drop to raw Odometer the moment you need more than the wrapper exposes.
Version pinning
With a score this low and a single GitHub star, pin the exact version in composer.json and commit the published assets. A stray composer update can drop a breaking provider rename onto a working dashboard.
How can I help you explore Laravel packages today?