Filtering
Lattice has two kinds of filter. Column filters compare a single column with an operator and live in that column’s header. Dedicated filters are named, table-level controls that own their own query logic. Both are sent to the table’s endpoint and applied by the data source.
TextColumn::make('name')->label('Name')->sortable()->filterable();NumberColumn::make('price')->label('Price')->sortable()->filterable();BooleanColumn::make('featured')->label('Featured')->filterable();TextColumn::make('updated_at')->label('Updated')->dateTime()->sortable();- actionsLabel:null
- bulkActions:[]
- data:[{"featured":true,"name":"Desk Lamp","price":"49.00","updated_at":"2026-05-30 09:15:00"},{"featured":false,"name":"Office Chair","price":"189.00","updated_at":"2026-06-02 14:40:00"},{"featured":true,"name":"Monitor Stand","price":"75.50","updated_at":"2026-06-08 08:05:00"}]
- emptyLabel:null
- endpoint:null
- filters:[]
- layout:null
- lazy:false
- pagination:{"currentPage":null,"from":1,"hasMore":false,"lastPage":null,"mode":"none","nextPage":null,"perPage":null,"to":3,"total":3}
- perPageOptions:[]
- pinnableColumns:false
- query:{"filters":[],"mode":null,"page":1,"perPage":25,"search":"","sorts":[],"tableFilterIndicators":[],"tableFilters":{}}
- resizableColumns:false
- resizeIndicator:false
- searchable:false
- striped:false
- toolbar:[]
- align:"start"
- badge:null
- copyable:false
- date:null
- hiddenByDefault:false
- label:"Name"
- link:null
- multiple:null
- options:[]
- pinned:null
- sortable:true
- toggleable:false
- width:"md"
- align:"end"
- compact:false
- copyable:false
- hiddenByDefault:false
- label:"Price"
- maximumFractionDigits:null
- minimumFractionDigits:null
- options:[]
- pinned:null
- sortable:true
- toggleable:false
- unit:null
- width:"md"
- align:"start"
- hiddenByDefault:false
- label:"Featured"
- options:[]
- pinned:null
- sortable:false
- toggleable:false
- width:"md"
- align:"start"
- badge:null
- copyable:false
- date:{"dateStyle":"medium","timeStyle":"medium"}
- filter:null
- hiddenByDefault:false
- label:"Updated"
- link:null
- multiple:null
- options:[]
- pinned:null
- sortable:true
- toggleable:false
- width:"md"
Column filters
Section titled “Column filters”->filterable() adds a filter for the column. The available operators come from the column’s filter
type, which is inferred from its display modifiers:
| Column | Filter type | Operators |
|---|---|---|
TextColumn (default) |
Text |
contains, starts_with, ends_with, =, ≠, empty, filled |
NumberColumn |
Number |
=, ≠, >, ≥, <, ≤, empty, filled |
TextColumn->date() |
Date |
=, before, after, empty, filled |
BooleanColumn |
Boolean |
=, empty, filled |
TextColumn::make('name')->filterable(); // text filterNumberColumn::make('price')->filterable(); // number filterTextColumn::make('created_at')->date()->filterable(); // date filterFilter types and operators
Section titled “Filter types and operators”Operators are the shared Op enum (Lattice\Core\Enums\Op) — the
same vocabulary used by form conditions. Each FilterType
offers a sensible default set and a default operator (text defaults to contains, the rest to =).
Narrow the offered operators, or change the default, by passing arguments to ->filterable():
use Lattice\Core\Enums\Op;
// Only allow exact / negated matches, defaulting to "equals":TextColumn::make('sku')->filterable(Op::Equals, [Op::Equals, Op::NotEquals]);A column’s filter capability serializes as a ColumnFilter ({ type, operators, defaultOperator, control, options, multiple, searchable, clauseOptions }) so the React filter control
knows which inputs and operators to render. Open the Tree tab on the example above to see it.
Filtering by a fixed set of options
Section titled “Filtering by a fixed set of options”When a column holds one of a known set of values (a status, a type), ->filterOptions() renders a
dropdown in the column header instead of the operator input — the same control used by the
dedicated SelectFilter. A single selection matches with =; pass
multiple: true to match any of the chosen values with in.
use Lattice\Table\Columns\TextColumn;
// associative value => labelTextColumn::make('status')->filterOptions([ 'draft' => 'Draft', 'active' => 'Active', 'archived' => 'Archived',]);
// or an enum (labels from the HasLabel contract, else the humanised case name)TextColumn::make('status')->filterOptions(Status::class);
// match any of several values:TextColumn::make('status')->filterOptions([...], multiple: true);filterOptions() implies filterable(), sets the column’s filter control to filter.select, and
restricts the offered operators to = / ≠ (or in / not in when multiple). The selected value
rides the same column-filter clause as any other operator filter, so the data source applies it with no
extra wiring.
Options can also come from an OptionSource (e.g. an Eloquent relation) instead of a fixed list — the
same source the Select field uses, so forms and tables resolve options the
same way:
use Lattice\EloquentOptions;
TextColumn::make('author_id') ->label('Author') ->filterOptions(EloquentOptions::make(Author::class)->label('name'));
// fetch options as the user types instead of shipping them all up front:TextColumn::make('author_id') ->filterOptions(EloquentOptions::make(Author::class)->label('name'), searchable: true);Use ColumnFilterOption when one dropdown choice should emit an operator other than the select
default. This keeps the request in the standard column-filter clause format, so the data source still
uses the same operator pipeline:
use Lattice\Core\Enums\Op;use Lattice\Table\Columns\ColumnFilterOption;
BooleanColumn::make('verified')->filterOptions([ ColumnFilterOption::clause('Yes', 'yes', Op::Equals, 'true'), ColumnFilterOption::clause('No', 'no', Op::Equals, 'false'), ColumnFilterOption::clause('Unset', 'unset', Op::Empty),]);
TextColumn::make('updated_at')->date()->filterOptions([ ColumnFilterOption::range('This month', 'this-month', '2026-06-01', '2026-06-30'),]);Dedicated filters
Section titled “Dedicated filters”Per-column filters compare a single column with an operator. Dedicated filters are named,
table-level controls rendered in the header’s filter dropdown — each owns its own control and query
logic, and isn’t tied to a column. Declare them by overriding filters():
use Illuminate\Database\Eloquent\Builder;use Lattice\Table\Filters\DateRangeFilter;use Lattice\Table\Filters\SelectFilter;use Lattice\Table\Filters\TernaryFilter;use Lattice\Table\Filters\ToggleFilter;
public function filters(): array{ return [ SelectFilter::make('status') ->options([ SelectFilter::option('Draft', 'draft'), SelectFilter::option('Active', 'active'), ]) ->multiple(), // match any selected → whereIn
TernaryFilter::make('featured') // yes / no / all ->trueLabel('Featured') ->falseLabel('Not featured'),
DateRangeFilter::make('created_at'), // from / until, inclusive
ToggleFilter::make('high_value') // simple on/off query ->query(fn (Builder $query): Builder => $query->where('price', '>', 1000)), ];}| Filter | Control | Applies |
|---|---|---|
SelectFilter |
dropdown | where (single) or whereIn (->multiple()); ->attribute() to remap |
TernaryFilter |
yes / no / all | boolean where, or custom ->queries(true: …, false: …) |
DateRangeFilter |
from / until | inclusive whereDate on each provided bound |
ToggleFilter |
toggle | your ->query() closure when on, or boolean where by default |
The filter dropdown opens from the icon button in the trailing header cell, where the actions heading would otherwise sit. Active dedicated and column filters share one bar of individually removable chips with a single Reset all that clears them both.
A SelectFilter’s options can come from an OptionSource instead of a fixed list, and
->searchable() fetches them as the user types rather than shipping the whole list up front. The
lookup is a search sub-request to the table endpoint with a namespaced _target —
filter:<key>.<field> for a dedicated filter’s field, column:<key> for a searchable column
filter — so filter keys and dot-keyed relation columns can never collide:
use Lattice\EloquentOptions;
SelectFilter::make('author') ->optionsFrom(EloquentOptions::make(Author::class)->label('name')) ->searchable();Custom dedicated filters
Section titled “Custom dedicated filters”For anything beyond the built-ins, extend the abstract Filter class. A filter’s schema() returns
normal form fields, so field modifiers like ->rules() are reused directly. Lattice validates those
rules before apply() receives the sanitized FormData.
use Illuminate\Database\Eloquent\Builder;use Lattice\Form\Components\NumberInput;use Lattice\Form\FormData;use Lattice\Table\Attributes\AsFilter;use Lattice\Table\Filters\Filter;
#[AsFilter('filter.price-band')]final class PriceBandFilter extends Filter{ public function schema(): array { return [ NumberInput::make('min', 'Min')->rules(['numeric', 'min:0']), NumberInput::make('max', 'Max')->rules(['numeric', 'min:0']), ]; }
public function apply(Builder $builder, FormData $data): void { if ($data->has('min')) { $builder->where('price', '>=', $data->float('min')); }
if ($data->has('max')) { $builder->where('price', '<=', $data->float('max')); } }
public function indicator(FormData $data): string|array|null { return null; }}Returning null from indicator() uses the default chips built from the active schema fields. Return
a string, an array of strings, or ['Label' => $value] pairs when you need custom indicator text. If
schema() returns an empty array, the client renders the filter as a simple toggle with a value
field; use ToggleFilter unless you need a custom subclass.
How values reach the server
Section titled “How values reach the server”Dedicated filter values post as objects under the filter key: tf[status][value]=active,
tf[status][value][]=… for multi-select, tf[created_at][from]=… / [until] for a range, and
tf[high_value][value]=1 for an empty-schema toggle. Unknown filter keys return a 422 response.
Known filters whose values fail their field rules are ignored instead of being applied.
After validation, the data source calls each active filter’s apply() method.
ToggleFilter::query() closures run server-side with utility injection — type-hint Builder to
receive the query. See Closure evaluation for the resolver contract.