haythem/progress-bar
A lightweight Laravel package for displaying customizable progress bars in Blade views. Configure colors, sizes, and values to visualize task completion, loading states, or any percentage-based progress with minimal setup.
This is a Laravel Nova field package — not a general Laravel package. It only does anything inside a Nova resource, dashboard, or tool. If you don't already have a Nova app (and a Nova license/nova private repo configured in composer.json), there is nothing here for you to evaluate yet.
Minimal steps:
composer require haythem/progress-bar
php artisan nova:publish # if the field's compiled assets don't show up
Nova fields register their JS/CSS through their service provider (Nova::script / Nova::style), so there is normally no manual provider registration — Laravel package discovery handles it. If the field renders as a blank cell, that registration step is the first thing to confirm.
First use case — drop it into a resource's fields() and feed it a number:
use Haythem\ProgressBar\ProgressBar;
public function fields(NovaRequest $request)
{
return [
ProgressBar::make('Onboarding')
->resolveUsing(fn () => $this->completedSteps() / max($this->totalSteps(), 1) * 100)
->help('Percent complete'),
];
}
Where to look first, in this order:
README.md — for the exact chainable API (this package is small; the README is the spec).src/ProgressBar.php — the field class. Everything that matters is here: $component, default meta, and any custom fluent setters.resources/js/components/*.vue (or dist/js/field.js if source isn't shipped) — the props it actually reads. Method names in the PHP class must match props in the Vue component; that correspondence is the whole API.composer.json — check the nova version constraint before planning anything.Because the package is tiny and single-maintainer, budget time to read all four. That's an afternoon, not a week.
Compute in the model, display in the field. The most robust pattern is to make the field dumb and put the math in an accessor or a resolveUsing closure. That keeps the percentage testable without booting Nova or a browser:
// Model
public function getStoragePercentAttribute(): float
{
return $this->quota_bytes > 0
? round($this->used_bytes / $this->quota_bytes * 100, 1)
: 0.0;
}
// Nova
ProgressBar::make('Storage', 'storage_percent')
->displayUsing(fn ($value) => $value . '%'),
Per-row progress on index views. This is the pattern people actually want — a progress bar per row in a resource index. It works, but only if the underlying numbers are already loaded. Pair it with withCount/aggregates on the resource's indexQuery, otherwise you get one query per row computing the percentage:
public static function indexQuery(NovaRequest $request, $query)
{
return $query->withSum('tasks as done_tasks', 'completed')
->withCount('tasks');
}
Detail-page layout. Wrap it in a Panel or Section so it reads as a metric rather than a stray field. It's most useful next to supporting numbers (Number::make('Used'), Number::make('Quota')) — a bar with no denominator visible is decoration.
Read-only by default. A progress bar is an output, not an input. Apply ->exceptOnForms() (or ->readonly()) so it doesn't appear in create/update forms as a confusing empty control.
Thresholds and colors. If the field exposes color meta, drive it from the model so business rules live in PHP, not in a Vue file you'd have to rebuild:
ProgressBar::make('Health')
->withMeta(['color' => $this->health_percent > 80 ? '#22c55e' : '#ef4444'])
When you outgrow it. The realistic path for a 1-star field is: use it for a week, then copy src/ and the Vue component into nova-components/ProgressBar with php artisan nova:field. You keep the behavior, gain a build you control, and stop depending on an unmaintained repo.
This package has one star and zero dependents. Treat it as a code sample, not infrastructure. Read every line of src/ProgressBar.php before shipping it; it is short enough that this is cheap. If you need it long-term, vendor-fork it into your own nova-components/ directory and note the origin in a comment.
Nova major-version mismatch is the most likely failure. Nova 4 (Laravel Mix) and Nova 5 (Vite) ship different asset pipelines. Check the nova constraint in composer.json against your installed version, and check whether dist/ exists or whether you're expected to npm install && npm run build inside vendor/. The latter is a smell — it means the release is not actually shippable as-is.
Blank field / no styles. Almost always one of: (a) the service provider didn't register the script, (b) assets need php artisan nova:publish, (c) the $component name in the PHP class doesn't match the registered Vue component name. Check the browser network tab for a 404 on the field JS before touching PHP.
Percent vs fraction. The single most common visual bug with progress bars is a 0–1 value fed to something expecting 0–100 (or vice versa) — you get either a permanently full bar or a sliver. Clamp defensively: max(0, min(100, $value)). Don't trust the model to always be in range.
Nulls and division by zero. A null value typically renders as 0% or an empty cell with no hint that data is missing. Coalesce to a sensible default and consider ->nullable()-style display text so "no data" is visually distinct from "0% complete."
Sorting and filtering don't exist here. A custom field is display-only. ->sortable() on a computed percentage will either error or silently sort by a nonexistent column. If you need sortable/filterable progress, persist the percentage to a real column (or computed column) and sort on that.
Index-view N+1. As above — the field resolves per row. Any resolveUsing that touches a relationship will fire per row. Verify with DB::listen or Telescope on a 50-row index before declaring victory.
No reactivity. Nova fields re-render on page load/navigation, not on data change. There's no built-in polling. If you want a live bar, either refresh the detail page or wire a setInterval + Nova.request() in the component — at which point you own the Vue code anyway.
Extension points are thin. You can override ->component('my-custom-bar') and register your own compiled component, and you can pass arbitrary data through ->withMeta([...]). Those two hooks cover most customization; anything beyond that is a fork.
How can I help you explore Laravel packages today?