Table
Filters
Wire Table provides 4 built-in filter types plus the ability to build custom filters. Filters live in the filter bar above the table and persist in Livewire state via $tableFilters.
Wire Table provides 4 built-in filter types plus the ability to build custom filters. Filters live in the filter bar above the table and persist in Livewire state via $tableFilters.
Table of Contents
- Filter Flow
- Shared Filter API
- SelectFilter
- DateFilter
- NumberRangeFilter
- TernaryFilter
- Column-Level Filters
- Custom Filter Class
- Patterns & Recipes
Filter Flow
Table::filters([...])│├── Render: Filter components in sidebar/header bar│ └── Each filter renders its own Blade view│├── State: $tableFilters array ['name' => 'value', ...]│ └── Persisted in Livewire component state│└── Apply: When state changes ├── Each filter's apply() or query() callback is called ├── Conditions added to Eloquent Builder └── Table re-queries with filters applied
Filter application flows through the Core ApplyFilters pipe in the QueryExecutor pipeline.
Shared Filter API
Every filter inherits from the base Filter class (289 lines).
Factory & Identity
Filter::make(string $name) // static factory->label(string|Closure $label) // display label (auto-generated from name)->getName(): string->getLabel(): string
Column Binding
->column(string $column) // DB column to filter on (defaults to $name)->getColumn(): string
When column() is not called, the filter uses its $name as the database column.
Custom Query Logic
->query(Closure $fn) // custom query callback
The callback signature: function (Builder $query, mixed $value): void
SelectFilter::make('activity_level') ->options([...]) ->query(function (Builder $query, string $value) { match ($value) { 'active' => $query->where('last_active_at', '>=', now()->subDays(7)), 'inactive' => $query->where('last_active_at', '<', now()->subDays(30)), 'new' => $query->where('created_at', '>=', now()->subDays(7)), }; })
Visibility & Permissions
->hidden(bool|Closure $hidden = true)->visible(bool|Closure $visible = true)->isHidden(): bool->permission(string $permission) // visible only if user has permission
DateFilter::make('deleted_at') ->from() ->until() ->permission('view-deleted-records') SelectFilter::make('internal_status') ->options([...]) ->visible(fn () => auth()->user()->is_admin)
Default Value
->default(mixed $value) // pre-selected on initial load->getDefault(): mixed
SelectFilter::make('status') ->options([...]) ->default('active') // "active" pre-selected
Multiple Selection
->multiple(bool $multiple = true)
When enabled, the filter accepts an array of values and applies whereIn().
View Customization
->view(string $view) // custom Blade view for the filter UI
SelectFilter
Dropdown filter for predefined options. The most common filter type.
use NyonCode\WireTable\Filters\SelectFilter;
Basic Usage
SelectFilter::make('status') ->options([ 'active' => 'Active', 'inactive' => 'Inactive', 'banned' => 'Banned', ])
With Placeholder
The first item is always an empty "All" option. To customize:
SelectFilter::make('role') ->options([ '' => 'All Roles', // explicit placeholder 'admin' => 'Admin', 'editor' => 'Editor', 'viewer' => 'Viewer', ])
Multiple Selection
SelectFilter::make('tags') ->options(Tag::pluck('name', 'id')->toArray()) ->multiple() ->label('Tags')
When multiple(), applies whereIn() instead of where().
Searchable Dropdown
SelectFilter::make('country') ->options(Country::pluck('name', 'code')->toArray()) ->searchable() ->label('Country')
Renders a searchable dropdown (Alpine.js powered) instead of native <select>.
Native HTML Select
SelectFilter::make('type') ->options([...]) ->native() // native <select> element (faster render)
From Database
SelectFilter::make('department') ->options(fn () => Department::orderBy('name')->pluck('name', 'id')->toArray())
Options can be a Closure — evaluated lazily on render.
Custom Query
SelectFilter::make('has_avatar') ->options([ 'yes' => 'With Avatar', 'no' => 'Without Avatar', ]) ->query(fn (Builder $query, string $value) => match($value) { 'yes' => $query->whereNotNull('avatar_url'), 'no' => $query->whereNull('avatar_url'), })
SelectFilter API
->options(array|Closure $options) // ['value' => 'Label', ...]->multiple(bool $multiple = true) // multi-select mode->searchable(bool $searchable = true) // searchable dropdown->native(bool $native = true) // native <select>
DateFilter
Date filter with single date or date range mode. Renders native date input(s).
use NyonCode\WireTable\Filters\DateFilter;
Single Date
DateFilter::make('created_at')// Applies: WHERE created_at = '2024-01-15'
Date Range (From/Until)
DateFilter::make('created_at') ->from() ->until()// Renders two date inputs: "From" and "Until"// Applies: WHERE created_at >= '2024-01-01' AND created_at <= '2024-01-31'
Only "From" or Only "Until"
// Only "from" — open-ended upper boundDateFilter::make('published_after') ->column('published_at') ->from()// Applies: WHERE published_at >= value // Only "until" — open-ended lower boundDateFilter::make('expires_before') ->column('expires_at') ->until()// Applies: WHERE expires_at <= value
Custom Labels
DateFilter::make('period') ->column('created_at') ->from() ->until() ->fromLabel('Created after') ->untilLabel('Created before')
Date Constraints
DateFilter::make('birth_date') ->minDate('1900-01-01') ->maxDate(now()->format('Y-m-d'))
DateFilter API
->from(bool $from = true) // enable "from" date input->until(bool $until = true) // enable "until" date input->fromLabel(string $label) // label for "from" input (default: 'From')->untilLabel(string $label) // label for "until" input (default: 'Until')->minDate(string $date) // min selectable date->maxDate(string $date) // max selectable date
Range Behavior
| from | until | Condition |
|---|---|---|
| set | null | WHERE column >= from |
| null | set | WHERE column <= until |
| set | set | WHERE column >= from AND column <= until |
| null | null | No filter applied |
NumberRangeFilter
Numeric range filter with min/max inputs. Renders two number inputs.
use NyonCode\WireTable\Filters\NumberRangeFilter;
Basic Usage
NumberRangeFilter::make('price') ->min(0) ->max(10000) ->step(0.01)
Integer Range
NumberRangeFilter::make('age') ->min(18) ->max(100) ->step(1)
Custom Labels
NumberRangeFilter::make('salary') ->min(0) ->max(500000) ->step(1000) ->minLabel('Minimum Salary') ->maxLabel('Maximum Salary')
Range Behavior
| min | max | Condition |
|---|---|---|
| set | null | WHERE column >= min |
| null | set | WHERE column <= max |
| set | set | WHERE column >= min AND column <= max |
| null | null | No filter applied |
NumberRangeFilter API
->min(float $min) // minimum allowed value->max(float $max) // maximum allowed value->step(float $step) // input step increment->minLabel(string $label) // label for min input (default: 'Min')->maxLabel(string $label) // label for max input (default: 'Max')
TernaryFilter
Three-state filter: Yes / No / All. Perfect for boolean columns and "has/doesn't have" relationships.
use NyonCode\WireTable\Filters\TernaryFilter;
Basic Boolean
TernaryFilter::make('is_active')// Shows: All | Yes | No// Yes: WHERE is_active = 1// No: WHERE is_active = 0
Nullable Column
TernaryFilter::make('email_verified_at') ->nullable()// Yes: WHERE email_verified_at IS NOT NULL// No: WHERE email_verified_at IS NULL
Custom Labels
TernaryFilter::make('verified') ->label('Verification Status') ->trueLabel('Verified Only') ->falseLabel('Unverified Only')
Custom Query Logic
TernaryFilter::make('has_orders') ->label('Has Orders') ->trueQuery(fn (Builder $query) => $query->has('orders')) ->falseQuery(fn (Builder $query) => $query->doesntHave('orders'))
TernaryFilter::make('overdue') ->label('Overdue') ->trueQuery(fn (Builder $query) => $query->where('due_at', '<', now())) ->falseQuery(fn (Builder $query) => $query->where('due_at', '>=', now()))
TernaryFilter API
->trueLabel(string $label) // default: 'Yes'->falseLabel(string $label) // default: 'No'->nullable(bool $nullable = true) // treat as IS NULL / IS NOT NULL->trueQuery(Closure $fn) // custom query for "Yes"->falseQuery(Closure $fn) // custom query for "No"
State Values
| UI State | Internal Value | Default Behavior |
|---|---|---|
| All | null |
No filter |
| Yes | true |
WHERE column = 1 or IS NOT NULL (if nullable) |
| No | false |
WHERE column = 0 or IS NULL (if nullable) |
Filtering by Relationships
Use ->query() with whereHas() to filter by related model attributes:
// BelongsTo — filter by related modelSelectFilter::make('category') ->options(Category::orderBy('name')->pluck('name', 'id')->toArray()) ->query(fn (Builder $query, string $value) => $query->whereHas('category', fn ($q) => $q->where('id', $value)) ) // BelongsToManySelectFilter::make('tags') ->options(Tag::orderBy('name')->pluck('name', 'id')->toArray()) ->multiple() ->query(fn (Builder $query, array $values) => $query->whereHas('tags', fn ($q) => $q->whereIn('id', $values)) ) // HasMany (existence) — use TernaryFilterTernaryFilter::make('has_comments') ->label('Has Comments') ->trueQuery(fn ($q) => $q->has('comments')) ->falseQuery(fn ($q) => $q->doesntHave('comments'))
Column-Level Filters
In addition to dedicated filter components, any column can have an inline filter directly in its header cell. See Columns — Column-Level Filtering.
TextColumn::make('status') ->filterable() ->filterAsSelect(['active' => 'Active', 'inactive' => 'Inactive']) TextColumn::make('price') ->filterable() ->filterAsNumberRange() ->filterMinValue(0) ->filterMaxValue(10000) TextColumn::make('created_at') ->filterable() ->filterAsDateRange()
Column filters use the $columnFilters Livewire property (separate from $tableFilters).
Custom Filter Class
For reusable, complex filters, extend the base Filter class.
Skeleton
namespace App\Wire\Filters; use NyonCode\WireTable\Filters\Filter;use Illuminate\Database\Eloquent\Builder; class MyFilter extends Filter{ // Custom properties protected string $myOption = 'default'; // Fluent setter public function myOption(string $value): static { $this->myOption = $value; return $this; } // Getter (for Blade view) public function getMyOption(): string { return $this->myOption; } // Override apply logic public function apply(Builder $query, mixed $value): Builder { if (empty($value)) { return $query; } // Your custom query logic return $query->where(...); } // Custom Blade view (optional) public function getView(): string { return 'filters.my-filter'; }}
Example: JSON Contains Filter
namespace App\Wire\Filters; use NyonCode\WireTable\Filters\Filter;use Illuminate\Database\Eloquent\Builder; class JsonContainsFilter extends Filter{ protected string $jsonPath = ''; public function jsonPath(string $path): static { $this->jsonPath = $path; return $this; } public function apply(Builder $query, mixed $value): Builder { if (empty($value)) { return $query; } $column = $this->getColumn(); if ($this->jsonPath) { return $query->whereJsonContains("{$column}->{$this->jsonPath}", $value); } return $query->whereJsonContains($column, $value); }}
Usage:
JsonContainsFilter::make('permissions') ->column('settings') ->jsonPath('permissions') ->options(['admin' => 'Admin', 'edit' => 'Edit', 'view' => 'View'])
Example: Geo Radius Filter
namespace App\Wire\Filters; use NyonCode\WireTable\Filters\Filter;use Illuminate\Database\Eloquent\Builder; class GeoRadiusFilter extends Filter{ protected float $defaultRadius = 10.0; protected string $latColumn = 'latitude'; protected string $lngColumn = 'longitude'; public function radius(float $km): static { $this->defaultRadius = $km; return $this; } public function coordinates(string $lat, string $lng): static { $this->latColumn = $lat; $this->lngColumn = $lng; return $this; } public function apply(Builder $query, mixed $value): Builder { if (empty($value['lat']) || empty($value['lng'])) { return $query; } $lat = (float) $value['lat']; $lng = (float) $value['lng']; $radius = (float) ($value['radius'] ?? $this->defaultRadius); // Haversine formula (returns km) $haversine = "(6371 * acos( cos(radians(?)) * cos(radians({$this->latColumn})) * cos(radians({$this->lngColumn}) - radians(?)) + sin(radians(?)) * sin(radians({$this->latColumn})) ))"; return $query ->whereRaw("{$haversine} <= ?", [$lat, $lng, $lat, $radius]); } public function getDefaultRadius(): float { return $this->defaultRadius; } public function getView(): string { return 'filters.geo-radius'; }}
Patterns & Recipes
E-commerce Product Filters
$table->filters([ SelectFilter::make('category') ->options(Category::orderBy('name')->pluck('name', 'id')->toArray()) ->searchable() ->query(fn (Builder $q, string $v) => $q->whereHas('category', fn ($q) => $q->where('id', $v))), SelectFilter::make('brand') ->options(Brand::orderBy('name')->pluck('name', 'id')->toArray()) ->searchable() ->multiple(), NumberRangeFilter::make('price') ->min(0) ->max(100000) ->step(100) ->minLabel('Min Price') ->maxLabel('Max Price'), TernaryFilter::make('in_stock') ->label('Availability') ->trueLabel('In Stock') ->falseLabel('Out of Stock') ->trueQuery(fn ($q) => $q->where('stock_quantity', '>', 0)) ->falseQuery(fn ($q) => $q->where('stock_quantity', '<=', 0)), SelectFilter::make('rating') ->options([ '5' => '★★★★★', '4' => '★★★★☆ and up', '3' => '★★★☆☆ and up', ]) ->query(fn (Builder $q, string $v) => $q->where('avg_rating', '>=', (int)$v)), TernaryFilter::make('has_discount') ->label('Discounted') ->trueQuery(fn ($q) => $q->whereNotNull('discount_percent')) ->falseQuery(fn ($q) => $q->whereNull('discount_percent')),]);
Admin User Filters
$table->filters([ SelectFilter::make('role') ->options(Role::pluck('display_name', 'name')->toArray()) ->multiple(), TernaryFilter::make('email_verified_at') ->label('Email Verified') ->nullable(), DateFilter::make('created_at') ->from() ->until() ->fromLabel('Registered after') ->untilLabel('Registered before'), TernaryFilter::make('two_factor_enabled') ->label('2FA Enabled'), SelectFilter::make('last_activity') ->label('Activity') ->options([ 'today' => 'Active today', 'week' => 'Active this week', 'month' => 'Active this month', 'inactive' => 'Inactive (30+ days)', ]) ->query(fn (Builder $q, string $v) => match($v) { 'today' => $q->whereDate('last_active_at', today()), 'week' => $q->where('last_active_at', '>=', now()->subWeek()), 'month' => $q->where('last_active_at', '>=', now()->subMonth()), 'inactive' => $q->where('last_active_at', '<', now()->subMonth()), }),]);
Order Management Filters
$table->filters([ SelectFilter::make('status') ->options([ 'pending' => 'Pending', 'processing' => 'Processing', 'shipped' => 'Shipped', 'delivered' => 'Delivered', 'cancelled' => 'Cancelled', 'refunded' => 'Refunded', ]) ->multiple() ->default(['pending', 'processing']), // pre-select active orders DateFilter::make('ordered_at') ->from() ->until(), NumberRangeFilter::make('total') ->min(0) ->max(1000000) ->step(100) ->minLabel('Min Total') ->maxLabel('Max Total'), SelectFilter::make('payment_method') ->options([ 'card' => 'Credit Card', 'bank' => 'Bank Transfer', 'cash' => 'Cash on Delivery', ]), TernaryFilter::make('has_notes') ->label('Has Notes') ->trueQuery(fn ($q) => $q->whereNotNull('notes')->where('notes', '!=', '')) ->falseQuery(fn ($q) => $q->where(fn ($q2) => $q2->whereNull('notes')->orWhere('notes', '') )),]);
Filter with Dependent Options
// Country → City cascade (requires Livewire re-render)SelectFilter::make('country') ->options(Country::pluck('name', 'id')->toArray()) ->searchable(), SelectFilter::make('city') ->options(function () { $countryId = $this->tableFilters['country'] ?? null; if (! $countryId) { return []; } return City::where('country_id', $countryId)->pluck('name', 'id')->toArray(); }) ->searchable() ->visible(fn () => ! empty($this->tableFilters['country'])),