Skip to content

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();

->filterable() adds a filter for the column. The available operators come from the column’s filter type, which is inferred from its display modifiers:

ColumnFilter typeOperators
TextColumn (default)Textcontains, starts_with, ends_with, =, ≠, empty, filled
NumberColumnNumber=, ≠, >, ≥, <, ≤, empty, filled
TextColumn->date()Date=, before, after, empty, filled
BooleanColumnBoolean=, empty, filled
TextColumn::make('name')->filterable(); // text filter
NumberColumn::make('price')->filterable(); // number filter
TextColumn::make('created_at')->date()->filterable(); // date filter

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.

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 => label
TextColumn::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'),
]);

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)),
];
}
FilterControlApplies
SelectFilterdropdownwhere (single) or whereIn (->multiple()); ->attribute() to remap
TernaryFilteryes / no / allboolean where, or custom ->queries(true: …, false: …)
DateRangeFilterfrom / untilinclusive whereDate on each provided bound
Filtertoggleyour ->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();

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.