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();- actionsLabel:"Actions"
- 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:"No results"
- endpoint:null
- filters:[]
- layout:null
- lazy:null
- pagination:{"currentPage":null,"from":1,"hasMore":false,"lastPage":null,"mode":"none","nextPage":null,"perPage":null,"to":3,"total":3}
- resizableColumns:null
- resizeIndicator:false
- state:{"filters":[],"page":1,"perPage":25,"sorts":[],"tableFilters":[]}
- striped:true
- badge:null
- copyable:false
- date:null
- link:null
- multiple:null
- compact:false
- maximumFractionDigits:null
- minimumFractionDigits:null
- unit:null
- badge:null
- copyable:false
- date:{"dateStyle":"medium","timeStyle":"medium"}
- link:null
- multiple:null
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\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\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 ({ enabled, 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\Lattice\Tables\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 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\Lattice\Core\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\Lattice\Core\Enums\Op;use Lattice\Lattice\Tables\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\Lattice\Tables\Filters\DateRangeFilter;use Lattice\Lattice\Tables\Filters\Filter;use Lattice\Lattice\Tables\Filters\SelectFilter;use Lattice\Lattice\Tables\Filters\TernaryFilter;
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
Filter::make('high_value') // custom toggle + 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 |
Filter | toggle | your ->query() closure when on |
The filter dropdown opens from the icon button in the trailing header cell, where the actions heading would otherwise sit. Active values show as removable chips with a Reset all that clears every filter — dedicated and column alike.
A SelectFilter’s options can come from an OptionSource instead of a fixed list, and
->searchable() fetches them as the user types (via a _search round-trip to the table endpoint)
rather than shipping the whole list up front:
use Lattice\Lattice\Core\EloquentOptions;
SelectFilter::make('author') ->optionsFrom(EloquentOptions::make(Author::class)->label('name')) ->searchable();How values reach the server
Section titled “How values reach the server”Dedicated filter values post as Laravel bracket params — tf[status]=active, repeated
tf[status][]=… for multi-select, and tf[created][from]=… / [until] for a range. They’re validated
against the declared filters, then applied by the data source. A custom
Filter::query() closure runs server-side with utility injection — type-hint Builder to receive the
query. See Closure evaluation for the resolver contract.