Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nova Pill Filter Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.
  • Your own 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).

Implementation Patterns

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'] ?? []);

Gotchas and Tips

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":

  1. Is it returned from filters(Request $request) on the resource (or registered in Nova::filters())?
  2. Does the browser console show a missing component? → asset/version mismatch.
  3. Does the URL change when you click a pill? → JS/component issue.
  4. Does the query change when the URL changes? → your apply() logic.

Debugging order for "reset does nothing" (v1.0.1 specific):

  1. Run composer show — are you really on v1.0.1, and did you clear compiled assets?
  2. Does the URL drop the filter key on reset? If not → component/JS side, not your code.
  3. Does the URL clear but the rows stay? → your apply() isn't handling null/empty gracefully.
  4. Still stuck? → check whether you overrode 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.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect