List view filter panel
A list view's filter panel, with owner scope, saved filters and filter logic.
Filters
Matching these filters
Installation
The CLI installs the Radix or Base UI file to match your project, with what it builds on: button, checkbox, date-picker, field, filter, input, input-group, list-view-filtering, number-input, radio-group, select, textarea, tooltip.
If your project doesn't have Wisty UI's theme yet, add it once with npx shadcn@latest add @wisty/cosmos. Until then, components keep shadcn's default look. Installation has the details.
When to use
Use it
- Beside a list view's table, to filter its records by owner and by field, as Lightning's Show filters panel does.
- When people add, edit and remove filters as a draft, and the list changes only when they press Save.
- When filters combine through numbered filter logic, like
1 AND (2 OR 3), checked and renumbered as Lightning's are. - When the page decides which fields can be filtered, with which operators, and who the signed-in user is.
- When a view has filters people can see but not remove, with the reason beside them.
Don't use it
- For the whole list page, with its header and table. Use List view, and dock this panel beside its table.
- For filters in an overlay that covers the page. Use Sheet.
- For nested groups of conditions, or cross-object filters. This panel is a flat, numbered list.
- To search the rows by text. Use List view header's search.
- For a field kind it doesn't take, like a date and time, a multi-select picklist or a relative date such as "this week".
Usage
import { ListViewFilters } from "@/components/wisty/list-view-filters"
import { applyListViewFilters } from "@/components/wisty/list-view-filtering"<ListViewFilters
id={filtersId}
schema={accountFilterSchema}
value={filters}
onValueChange={setFilters}
open={filtersOpen}
onOpenChange={setFiltersOpen}
/>The install writes the panel's code into the project: list-view-filters.tsx holds ListViewFilters and the useListViewFilters hook, and list-view-filters-editor.tsx holds the filter's Field, Operator and Value editor and the owner's radio group. The panel is plain JSX over Filter's parts, so to drop Filter by Owner, put the locked filters first or swap an editor for one with a lookup, edit those lines. useListViewFilters keeps the draft, its errors and Save in one place, as the Locked filters first example shows.
schema lists the fields people can filter by, the owner choice and any locked filters, and value is the saved { owner, filters, logic }. Both are List view filtering's types, and its page has the field kinds, the filter logic syntax with its messages, and how removing a filter renumbers the rest.
The editor's Save changes the panel's draft, and its Cancel drops the editor's changes but keeps a new filter. The owner's editor applies its choice on Done. The panel's Save calls onValueChange with the whole value, and its Cancel brings back the saved one. Apply the saved value to the rows before the table gets them, and show the result's state before a count:
const { props: filtered } = applyListViewFilters(
accounts,
filters,
accountFilterSchema
)
const list = useListView({ records: filtered.records, columns, getRowId })The panel checks the filter logic when the field loses focus, when a filter is added or removed, and on Save. Typing alone doesn't change the error shown. Add Filter hides at 10 filters, and the field stops at 120 characters. Removing the last filter turns filter logic off, and Save drops a new filter that has no field yet, through the same rewrite.
Close the panel by passing open={false}. It keeps its draft until it opens again. Give the button that opens it aria-controls with the panel's id, aria-expanded and aria-pressed, and move focus back to it in onOpenChange.
Examples
Opportunity criteria
Closed, Amount and Close Date filters with typed values, combined by the filter logic 1 AND (2 OR 3).
Filters
Matching these filters
Locked filters
A required filter stays in place with its reason, and Remove All clears only the editable ones.
Filters
Matching all of these filters
Locked Filters
This view shows open cases only.
Locked filters first
The panel's JSX edited over useListViewFilters: locked filters above the editable ones, with no Filter by Owner or filter logic.
Filters
Locked Filters
This view shows open cases only.
Matching all of these filters
Unavailable criteria
A saved filter on a field the list no longer offers is named as an error and can be removed without guessing its value.
Filters
Filters could not be applied. Please fix the validation errors below.
Matching all of these filters
Industry isn't available to filter by.
API reference
ListViewFilters
| Prop | Type | Default | Description |
|---|---|---|---|
schema* | ListViewFilterSchema<Row> | null | What people may filter by, or null while it isn't available. | |
value | ListViewFiltersValue | The saved filters, controlled. Only the panel's Save calls onValueChange; editors change the draft. | |
defaultValue | ListViewFiltersValue | The saved filters at first, uncontrolled. Default: all records, no filters. | |
onValueChange | (value: ListViewFiltersValue) => void | Called with the filters when Save succeeds. | |
open | boolean | true | Shows the panel. Closed, it keeps changes not yet saved. Default true. |
onOpenChange | (open: boolean) => void | Called when the close button is pressed; the button shows only with it. | |
title | string | "Filters" | The panel's heading. |
locale | string | "en-US" | Formats numbers, amounts and dates. |
Also takes every <section> (opens in a new tab) prop.
useListViewFiltershook
Holds a list view's saved filters and the panel's draft of them, with Lightning's checks and rewrites.
| Parameter | Type | Default |
|---|---|---|
__0* | UseListViewFiltersOptions<Row> |
Returns UseListViewFiltersResult.
UseListViewFiltersOptionstype
| Member | Type | Default | Description |
|---|---|---|---|
schema* | ListViewFilterSchema<Row> | null | What people may filter by, or null while it isn't available. | |
value | ListViewFiltersValue | The saved filters, controlled. Save calls onValueChange. | |
defaultValue | ListViewFiltersValue | The saved filters at first, uncontrolled. Default: all records, no filters. | |
onValueChange | (value: ListViewFiltersValue) => void | Called with the filters when Save succeeds. |
ListViewFiltersRowtype
A filter as the panel shows it, with its error once it should show.
type ListViewFiltersRow = DraftRow & {
error: string | undefined
}ListViewFiltersSaveResulttype
What Save did: saved, found nothing to save, or refused with the first filter key in error, else "logic", else "owner".
type ListViewFiltersSaveResult =
| "saved"
| "unchanged"
| "conflict"
| { invalid: number | "logic" | "owner" }UseListViewFiltersResulttype
| Member | Type | Default | Description |
|---|---|---|---|
rows* | readonly ListViewFiltersRow[] | The editable filters, numbered by position from 1. | |
owner* | "all" | "mine" | The owner scope shown. | |
ownerChanged* | boolean | The owner scope was changed in this draft. | |
ownerError* | string | Why the owner scope can't be applied, once it should show. | |
lockedError* | (key: string) => string | Why a locked filter can't be applied, by its key. | |
logic* | string | null | The filter logic shown, or null while it's off. | |
logicError* | string | The logic's error, as it was when last checked. | |
numbered* | boolean | The logic has text, so the filters show their numbers. | |
dirty* | boolean | Something differs from the saved filters. | |
conflict* | boolean | The saved filters changed under this draft, so Save is refused. | |
hasErrors* | boolean | Some error shows, so the panel says the filters can't be applied. | |
canAdd* | boolean | Fewer filters than the limit, so Add Filter can add one. | |
add* | () => number | null | Appends a blank new filter and returns its key, or null at the limit. | |
replace* | (key: number, condition: ListViewFilterCondition) => void | Replaces a filter's condition, marking it changed when it differs. | |
remove* | (key: number) => void | Removes a filter, renumbering the rest and rewriting the logic. | |
removeAll* | () => void | Removes every editable filter and turns logic off. | |
setOwner* | (owner: "all" | "mine") => void | Sets the owner scope in the draft. | |
addLogic* | () => void | Turns logic on, with every filter ANDed. | |
removeLogic* | () => void | Turns logic off. | |
typeLogic* | (logic: string) => void | Changes the logic text without checking it. | |
checkLogic* | () => void | Checks the logic as it is now. | |
save* | () => ListViewFiltersSaveResult | Drops blank filters, then saves the draft unless it's invalid or the saved filters changed under it. | |
cancel* | () => void | Drops the draft, back to the saved filters. |
SLDS mapping
The panel is Lightning's list view filter panel, built from plain SLDS parts: slds-panel slds-panel_docked slds-panel_docked-right with slds-panel__header (the title and slds-panel__close, or Cancel and Save while there are changes), a gray scroll area holding the white slds-panel__body, and the filter logic under it. Each filter is a Filter, slds-filters__item, with slds-is-new while it's new or changed, slds-has-error when it can't be applied and slds-is-locked for a locked filter. The editor is slds-popover slds-popover_large with slds-nubbin_right-bottom, the assistive heading "Choose filter criteria", picklists drawn as slds-combobox, and Cancel and Save. The owner's editor is an slds-radio group with Done.
Known gaps:
- The panel uses lucide's X, trash and info glyphs where Lightning draws
utility:close,utility:deleteandutility:info. - Lightning's Save has a menu with Save As. This panel saves only the list view it shows.
- Filter logic is one text field, without the expression builder's AND and OR rows.
- The panel doesn't animate in. It shows and hides with
open.
Keyboard interactions
| Key | Action |
|---|---|
| Enter, Space | On a filter, opens its editor. |
| Escape | Closes an open picker or calendar first, then the editor, without keeping its changes. The panel stays open. |
| Tab | Moves through the editor's fields, Cancel and Save. In Radix projects focus stays in the editor until it closes. In Base UI projects Tab past Save closes it. |
Accessibility
- The panel is a region named by its heading. When it opens from closed, focus moves to its close button, or to the heading while it holds changes that aren't saved. After Save or Cancel focus returns to the heading.
- After a filter is removed, focus moves to the next filter, or to Add Filter after the last one. After the editor's Save, focus moves to the panel's Cancel.
- A filter that can't be applied describes its button with the error, and Save focuses the first one.
- A new or changed filter shows an asterisk and its yellow background, and tells screen readers "Not saved" or "Changed, not saved" in words. A changed owner scope is yellow too.
- Add Filter Logic moves focus to the Filter Logic field.
- A locked filter's button is disabled, so its reason is shown as text under it.
- Two panels on one page need different headings, so their regions have different names. Pass
title.
Responsive behavior
- The panel takes its container's width and has no minimum. Give it
w-80against a table's right edge on a wide page, and the full width under the table on a narrow one, with a container query. - Give the panel a height or a
max-h-*. Its body then scrolls under the header. To run it the table's full height without stretching the table, put it in arelativewrapper beside the table and give itabsolute inset-0, as the Accounts page does. - The editor is 25rem wide, or narrower on a small screen. It opens to the panel's left, pointing at the filter, and to another side when the left has no room.
Rules
- Keep
undefinedfor a field the app didn't read andnullfor a blank one, in the schema'sread. Unread values never match, so the result ispartialinstead of a guess. - Locked filters always apply, outside the filter logic. They're how the page shows a filter, not a permission check: enforce access where the data is read.
- Call
useListViewFilterswhere it stays mounted while the panel is closed, asListViewFiltersdoes, or closing loses the draft. - Never pass
asChildorrender.
Related
- List viewA list of records: list views, search and a table to sort and select.
- List view headerThe top of a list page, where people switch list views, search and act.
- List view filteringThe rows a list view's saved filters keep, with Lightning's filter logic.
- FilterShows one filter criterion that opens an editor when pressed.
- SheetSlides a panel in from the edge of the screen for a task beside the page.