digital-creative/nova-pill-filter
A Laravel Nova package that adds customizable pill-style filters to resource index screens, letting users quickly toggle predefined filters through a clean, compact interface. Simple to configure and ideal for streamlining resource navigation in Nova admin panels.
1. Install (Nova projects only).
composer require digital-creative/nova-pill-filter:^1.0.1
Pin at least ^1.0.1. The v1.0.1 patch fixes "reset not working properly" — the reset control in the filter bar did not clear the filter state on v1.0.0 and earlier, which reads to users as a filter that is impossible to switch off. Do not accept a lockfile resolution to v1.0.0.
Nova packages are auto-discovered; there is typically no publish step. Still confirm nothing is required by checking the package README and its composer.json for a laravel/nova constraint — the release cadence is low (v1.0.0 dates to October 2023, v1.0.1 is a single-bugfix release), so verify it against your Nova major version (Nova 4 vs 5) before you commit.
2. Where to look first.
README.md in the package — the exact base class name and any pill-specific builder methods (colors, multi-select) are documented there and are the fastest source of truth.src/ — the filter base class you extend, and the Vue component it points Nova at.CHANGELOG / the GitHub compare view between tags — v1.0.1 has exactly one change (PR #35, reset), so this is a five-second read that tells you whether the version you have is the one you want.app/Nova/Filters/ — this is where all your subclasses will live.3. Generate a filter and swap the parent class.
php artisan nova:filter StatusFilter
Then change the generated class from Laravel\Nova\Filters\Filter to the package's pill filter base class:
namespace App\Nova\Filters;
use DigitalCreative\NovaPillFilter\PillFilter; // confirm exact namespace in README
use Illuminate\Http\Request;
class StatusFilter extends PillFilter
{
public function apply(Request $request, $query, $value)
{
return $query->where('status', $value);
}
public function options(Request $request)
{
return [
'Active' => 'active',
'Inactive' => 'inactive',
];
}
}
4. Register it on the resource.
public function filters(Request $request)
{
return [
new StatusFilter,
];
}
5. Click a pill, then click reset.
This is the smoke test that matters on v1.0.1. Click a pill (URL gains the filter key, rows change), then hit reset: the URL should drop the filter key and the index should return to its unfiltered state. On v1.0.0 the second half of that sequence was the broken part — the pill stayed highlighted and/or the query stayed filtered. If you are upgrading, re-run this by hand once; it is the exact behaviour the patch claims to fix.
First use case. A status/priority/segment filter on an index view. The pill UI shines when there are 2–6 mutually exclusive values users toggle constantly — status, tier, "needs review", environment — and the default Nova dropdown feels heavy. Start there, not with 30-value enums (that's what searchable select filters are for).
Single-select with a default. Pill filters still obey the normal filter contract, so ->default() and ->name() work as usual:
(new StatusFilter)->default('active')->name('Status'),
Keep $name short — it's a pill label rendered in a horizontal row, not a form label.
Multi-select / array values. If the package exposes multi-select, options() returns an associative array and Nova hands your apply() an array. Normalize defensively so the filter degrades gracefully if only one pill is selected:
public function apply(Request $request, $query, $value)
{
$values = array_filter((array) $value);
return $values === []
? $query
: $query->whereIn('status', $values);
}
This is the single most common pattern you'll write, and the array_filter((array) $value) guard costs nothing and saves a debugging session. It also absorbs the cleared/reset path, which is now a genuine null rather than a stale value.
Colour semantics per option. Pill filters usually let you map values to Nova's palette (success, info, warning, danger, plus the default primary). Use it consistently across resources — green for active/paid, amber for pending, red for failed — so operators can scan the filter bar at a glance:
// Pattern: a small lookup the filter consults when rendering.
protected array $colors = [
'active' => 'success',
'pending' => 'warning',
'inactive' => 'danger',
];
Confirm the exact method name (colors(), withMeta(), or per-option arrays) in the README — this is the part most likely to differ between versions.
Shared base filter. With more than two pill filters, extract a PillFilter of your own extending the package class, with helpers for the colour map and array normalization. That keeps each concrete filter down to options() + apply(), and if the package ever breaks on a Nova upgrade you have one place to patch.
Pair with a search/date filter, not with another pill filter. Multiple pill filters side by side get visually noisy and the selected state becomes hard to parse. Reserve pills for one or two high-frequency toggles and leave the rest in Nova's standard filter panel.
Lenses. Pill filters compose cleanly with lenses because they only touch the query in apply(). Register the filter on the resource (or globally in Nova::filters()) and switch lenses as usual. Worth checking once that lens switching plus reset behaves — reset semantics are the one thing that changed in v1.0.1, and lenses are the stateful context most likely to expose an edge case.
Dynamic options from the database. Fine, but memoize the query result inside the request — options() can be called more than once per page load (filter bar plus stored filter state):
protected ?array $cached = null;
public function options(Request $request)
{
return $this->cached ??= Status::pluck('name', 'slug')->all();
}
Testing apply() directly. You don't need browser tests to trust the query logic. Instantiate it and assert on the builder:
$query = Status::query();
(new StatusFilter)->apply(request(), $query, ['active']);
$this->assertSame(['active'], $query->toBase()->bindings['where'][0]);
Testing the reset path. Since reset shipped broken once, add the cheap regression test alongside it — cleared state must be a no-op, not an over-restrictive whereIn:
$query = Status::query();
(new StatusFilter)->apply(request(), $query, null);
$this->assertSame(Status::query()->toSql(), $query->toSql());
$this->assertSame([], $query->toBase()->bindings['where'] ?? []);
Upgrade explicitly to ^1.0.1. The reset fix is the only change in the release, which makes the version bump easy to skip and hard to notice when missing. Check what you actually have with composer show digital-creative/nova-pill-filter; if it resolves to v1.0.0, users will report that filters "can't be turned off" and you'll waste an afternoon in apply() looking for the bug.
Don't rely on it being actively maintained. v1.0.1 is a single community bugfix on top of an October 2023 base — good news, but not a sign of a roadmap. It's MIT and tiny. Fork it into your org, or vendor the base class into app/Nova/Filters/, if a Nova upgrade blocks you. This is a ~100-line concept, not a framework — the lock-in risk is near zero as long as you keep your subclasses thin.
Verify Nova compatibility before installing. Low release cadence means the compiled Vue asset has not been rebuilt against recent Nova majors. If you're on Nova 5, install on a branch first and open an index page; a Vue asset mismatch shows up as a blank or unstyled filter bar and JavaScript console errors, not a PHP exception.
apply() must return the query. Returning nothing silently produces an unfiltered list with no error. If pills highlight but rows don't change, this is the first thing to check.
Empty vs. null vs. reset. Nova sends null when a filter is cleared and may send an empty string or empty array in other paths. Since v1.0.1 reset is supposed to deliver that clean null — but always guard before whereIn(), because whereIn('status', []) returns zero rows, which looks like a broken filter to users. This is also the failure mode you'd see if the reset bug regressed: the UI clears, the value does not, the query stays empty.
Colour values must be from Nova's palette. Arbitrary strings typically render as the fallback colour with no warning. Stick to Nova's named colours so pills stay legible in both light and dark mode; custom hex values need manual contrast checking.
Watch pill label length. Long option labels overflow the pill on narrow screens. Keep them to one or two words, or shorten the display label in options() while keeping the underlying stored value.
Filter keys are in the URL. Nova persists filter state in the query string, so pill selections are shareable and bookmarkable — a real benefit for support links, and precisely why reset has to work: users paste a filtered link into a ticket, and the recipient needs an obvious way back to the full list. Don't rename the filter class or $name casually on a page users link to; the key becomes part of the URL contract.
Unique names across the filter bar. Two filters with the same name() collide in the UI and in the query string. Name them explicitly (Status, Tier) rather than relying on class basenames.
Cache-bust after upgrades. If styles vanish after bumping to v1.0.1, run php artisan nova:publish and clear your compiled views/assets. Compiled Vue from a stale package build is the usual culprit — and don't judge the reset fix until you've cleared it, or you'll be testing the old bundle.
Debugging order for a filter that "doesn't show up":
filters(Request $request) on the resource (or registered in Nova::filters())?apply() logic.Debugging order for "reset does nothing" (v1.0.1 specific):
composer show — are you really on v1.0.1, and did you clear compiled assets?apply() isn't handling null/empty gracefully.key() or name() in a way that desyncs persisted state from the class basename.That progression separates front-end from back-end failure in about thirty seconds, and splits the specific class of bug this release addresses.
Extension points. Override name(), key(), or options() in your subclass for behaviour; use the package's colour/meta builder for appearance. If you need a genuinely different look, publish/replace the Vue component rather than fighting the base class — but at that point, a custom Nova card or a plain Filter with a custom field may be the better investment.
How can I help you explore Laravel packages today?