Filtering and sorting
Advanced filters, default filter values, column sorting, quick filters and pinned filter chips.
Advanced filters
defineListConfig<Product>({
id: 'products',
search: true,
filtersTitle: 'Filter products',
filters: [
{
id: 'attributes',
title: 'Attributes',
filters: [
{
id: 'category',
field: 'category',
label: 'Category',
type: 'select',
options: [{ value: 'coffee', label: 'Coffee' }],
},
{
id: 'tags',
field: 'tags',
label: 'Tags',
type: 'multi-select',
options: [...],
},
{
id: 'price',
field: 'price',
label: 'Price',
type: 'number-range',
},
{
id: 'createdAt',
field: 'createdAt',
label: 'Created',
type: 'date-range',
},
{
id: 'active',
field: 'active',
label: 'Status',
type: 'boolean',
},
{
id: 'name',
field: 'name',
label: 'Name',
type: 'text',
},
],
},
],
})Applied filters appear as removable chips above the list and sync to the URL. With an async adapter, read query.filters (an ActiveFilterValue[]) in your fetcher and translate to SQL/HTTP.
Default filter values
Give any filter a defaultValue to pre-apply it on a pristine list — when the URL has no filters yet. Multiple filters can each set one (e.g. show only active rows and default to the current month):
const filters: FilterDefinition<Order>[] = [
{
id: 'status',
field: 'status',
label: 'Status',
type: 'select',
options: statusOptions,
defaultValue: 'active',
},
{
id: 'created',
field: 'createdAt',
label: 'Created',
type: 'date-range',
defaultValue: { from: '2026-06-01', to: '2026-06-30' },
},
]defaultValue uses the same shape the adapter receives for that type: select → string, multi-select → string[], boolean → boolean, text → { value, match }, date-range → { from?, to? }, number-range → { min?, max? }.
A date-range the picker produces carries absolute instants (…T06:00:00.000Z), not bare days: the operator picks a local day, and only the client knows their timezone, so it converts before the value travels — from at local 00:00, to at local 23:59:59.999. Left as a bare YYYY-MM-DD, the server read it as UTC midnight and shifted the whole window for anyone west of UTC. A hand-written YYYY-MM-DD still works and still means the whole day: both the in-memory matcher and the Mongo builder extend a date-only to to end-of-day, and neither touches a value that already carries a time.
Defaults seed the initial view only: they're applied on the first render (so the first fetch already includes them) and written to the URL, after which the user's edits/clears always win. Lists rendered with initialData (SSR) are left as-is — apply the defaults in your server query instead.
Column sorting
Mark any table column sortable. Clicking its header cycles ascending → descending → off, syncs the active sort to a sort URL param, and flows into the adapter as query.sort ({ field, dir }):
table: {
columns: [
{ key: 'name', header: 'Name', sortable: true },
{ key: 'createdAt', header: 'Created', sortable: true, sortField: 'created_at' },
{ key: 'total', header: 'Total', align: 'right', sortable: true },
],
}- In-memory data — the built-in adapter sorts automatically by the active field.
- Async adapters — read
query.sortin your fetcher and translate toORDER BY. sortFieldoverrides the field name sent to the adapter (defaults to the columnkey).
After a page loads, the next page is prefetched on idle into the cache, so clicking "next" renders instantly with no loading flash.
Quick filters
Mark a filter quick and it renders as a compact pill under the search box
that opens its real input in a popover — the frequently-used filters
without a trip to the sidebar. Users hide the bar from the Options menu.
{ id: 'channel', field: 'channel', label: 'Channel', type: 'select', options, quick: true }quick and pinned complement each other: a pinned chip toggles one
predetermined value (only unpaid), a quick pill lets the user pick any value
the filter accepts. Both stay ordinary filters — same URL param, same
query.filters entry, same cache key.
The sidebar also reorders itself around use: applied filters lead their
section, sections holding them lead the panel, and long untouched sections
start collapsed (never one holding an applied filter). Turn either off with
filtersActiveFirst: false / filtersAutoCollapse: false.
Pinned filter chips
Some filters are the list ("only unpaid", "active users"). Mark one pinned and it also renders as a toggleable chip above the rows:
{ id: 'paid', field: 'paid', label: 'Paid', type: 'boolean', pinned: true },
{ id: 'status', field: 'status', label: 'Status', type: 'select',
options, pinned: true, pinnedValue: 'pending' },Clicking applies pinnedValue (or defaultValue, or true for a boolean); clicking again clears it. It stays an ordinary filter — same URL param, same query.filters entry, same cache key — so nothing else in the list needs to know.