eolica/nova-pill-filter
Nova Pill Filter adds a pill-style filter to Laravel Nova resource indexes, letting users toggle predefined options with a single click instead of opening a dropdown. Lightweight and quick to register in your Nova resource.
This is a very small, single-purpose Nova filter that swaps the default <select> dropdown for a row of clickable pills. It has one star, zero dependents, and was archived in late 2020 — treat it as reference code you adopt, not a dependency you trust to be patched.
Step 1 — Read the source before installing.
git clone https://github.com/Eolica-Web/nova-pill-filter /tmp/pill
cat /tmp/pill/composer.json # what laravel/nova constraint?
find /tmp/pill -type f -not -path '*/node_modules/*'
You are looking for three things:
require block — if it targets laravel/nova: ^2.0|^3.0, it is written against Nova 3's filter contract (Vue 2, filter/value props, change event). Nova 5's Vue 3 tooling will not mount it.Nova::script() / Nova::style() (or relies on auto-discovery plus mix/compiled assets in dist/).pill-filter). That name is what you put in public $component.Step 2 — Install.
composer require eolica/nova-pill-filter
Since it is archived, Composer may resolve oddly; if so, add a vcs repository entry and require dev-master. If auto-discovery is absent, register the provider in config/app.php, then:
php artisan vendor:publish --provider="Eolica\NovaPillFilter\...ServiceProvider"
php artisan nova:publish
Step 3 — First use case. The canonical win is a status/state filter on an index screen: All · Active · Paused · Archived as pills, one click instead of open-dropdown-scroll-click.
php artisan nova:filter StatusFilter
// app/Nova/Filters/StatusFilter.php
namespace App\Nova\Filters;
use Illuminate\Http\Request;
use Laravel\Nova\Filters\Filter;
class StatusFilter extends Filter
{
public $name = 'Status';
// The whole package, in one line:
public $component = 'pill-filter';
public function apply(Request $request, $query, $value)
{
if ($value === null || $value === '') {
return $query;
}
return $query->where('status', $value);
}
public function options(Request $request)
{
return [
'All' => '',
'Active' => 'active',
'Paused' => 'paused',
'Archived' => 'archived',
];
}
}
Register it in filters() on the resource or lens. If nothing renders, the problem is almost always the component name (Step 1, item 3) or unpublished assets.
Subclass per concept, not per screen. Pills are cheap, so one filter class per boolean-ish dimension is the natural grain:
public function options(Request $request)
{
return [
'Any' => '',
'With trashed' => 'with',
'Only trashed' => 'only',
];
}
public function apply(Request $request, $query, $value)
{
return match ($value) {
'with' => $query->withTrashed(),
'only' => $query->onlyTrashed(),
default => $query,
};
}
Date presets are the strongest fit. A dropdown hides "This month", pills advertise it:
public function options(Request $request)
{
return [
'Today' => 'today',
'This week' => 'week',
'This month'=> 'month',
'This year' => 'year',
];
}
public function apply(Request $request, $query, $value)
{
$range = match ($value) {
'today' => [now()->startOfDay(), now()],
'week' => [now()->startOfWeek(), now()],
'month' => [now()->startOfMonth(), now()],
'year' => [now()->startOfYear(), now()],
default => null,
};
return $range
? $query->whereBetween('created_at', $range)
: $query;
}
Pre-select a pill with default() so the UI never looks "unfiltered but filtered":
public function default()
{
return 'active';
}
Keep options to ~6. Pills wrap, and a wrapped row of twelve looks worse than the dropdown you replaced. If you need more, split into two filters (Status + Date) rather than growing one.
Per-lens and per-resource reuse. Filters live in filters() on both Resource and Lens; the same pill filter instance can be shared, which is the main appeal for a dashboard-heavy app. Note each filter change resets pagination to page 1 and re-issues the /nova-api/{resource} request, so pills are fine for a handful of filters but multiply payloads if you stack many.
Fork for styling. There is no config to theme. The UI is the .vue file — copy it into resources/js/nova/components/ in your app, register it with Nova.booting(), and point $component at your own name. That also insulates you from the archive.
Integration tip: pills compose cleanly with Nova's native filters. Use pills only for the dimensions users toggle constantly, and leave long lists (users, teams, categories) as searchable selects — a nine-pill row of team names is unusable.
Archived, 1 star, 0 dependents. No fixes will come. It is likely a few dozen lines; budget an hour to vendor it into your app rather than fight an abandoned package on a Nova upgrade. The escape hatch is trivial: delete public $component and you're back to a stock Nova select filter with the same apply() semantics.
Version compatibility is the real risk. Released 2020-10-29, so it targets Nova 2/3. Before adopting, verify the Vue component mounts on your Nova version in a throwaway environment. Nova's filter JS contract (filter, value props; change emit) has been stable, but asset registration and Vue version have not. If it breaks, a custom component that emits change on click is roughly 40 lines.
Type coercion will bite you. Filter values round-trip through the query string/JSON. where('status', '1') against a Postgres integer column is fine in some drivers and fatal in others; cast explicitly in apply() ((int) $value, Carbon::parse($value)). The same applies to the value the Vue component emits — if it emits the label instead of the value, everything silently returns zero rows.
Empty value must mean "no filter." Nova sends null or '' for a cleared filter. Guard it, or you get where('status', '') and an empty table that looks like a bug in your code.
Debugging: open the network tab, click a pill, inspect the filters payload of the nova-api request — Nova JSON/base64-encodes filter values, and seeing the encoded value tells you instantly whether the component emitted the right thing. On the server side, Log::info($value) at the top of apply() confirms what actually arrived. If apply() never fires, the filter isn't registered on that resource/lens; if it fires with the wrong value, the bug is in the component.
Assets not updating? After editing the Vue file or republishing, run php artisan nova:publish and hard-reload the browser — Nova's compiled assets and browser cache are the two most common false alarms.
Extension points worth knowing: the component name ($component), default() for pre-selection, and apply() for logic — that's the whole surface. Anything visual requires forking the .vue file.
How can I help you explore Laravel packages today?