Skip to content

List view filter panel

A list view's filter panel, with owner scope, saved filters and filter logic.

Filters

Matching these filters

Installation

pnpm dlx shadcn@latest add @wisty/list-view-filters

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

  1. 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

  1. 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

Matching all of these filters

  1. Industry isn't available to filter by.

API reference

ListViewFilters

PropTypeDefaultDescription
schema*ListViewFilterSchema<Row> | nullWhat people may filter by, or null while it isn't available.
valueListViewFiltersValueThe saved filters, controlled. Only the panel's Save calls onValueChange; editors change the draft.
defaultValueListViewFiltersValueThe saved filters at first, uncontrolled. Default: all records, no filters.
onValueChange(value: ListViewFiltersValue) => voidCalled with the filters when Save succeeds.
openbooleantrueShows the panel. Closed, it keeps changes not yet saved. Default true.
onOpenChange(open: boolean) => voidCalled when the close button is pressed; the button shows only with it.
titlestring"Filters"The panel's heading.
localestring"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.

ParameterTypeDefault
__0*UseListViewFiltersOptions<Row>

Returns UseListViewFiltersResult.

UseListViewFiltersOptionstype

MemberTypeDefaultDescription
schema*ListViewFilterSchema<Row> | nullWhat people may filter by, or null while it isn't available.
valueListViewFiltersValueThe saved filters, controlled. Save calls onValueChange.
defaultValueListViewFiltersValueThe saved filters at first, uncontrolled. Default: all records, no filters.
onValueChange(value: ListViewFiltersValue) => voidCalled 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

MemberTypeDefaultDescription
rows*readonly ListViewFiltersRow[]The editable filters, numbered by position from 1.
owner*"all" | "mine"The owner scope shown.
ownerChanged*booleanThe owner scope was changed in this draft.
ownerError*stringWhy the owner scope can't be applied, once it should show.
lockedError*(key: string) => stringWhy a locked filter can't be applied, by its key.
logic*string | nullThe filter logic shown, or null while it's off.
logicError*stringThe logic's error, as it was when last checked.
numbered*booleanThe logic has text, so the filters show their numbers.
dirty*booleanSomething differs from the saved filters.
conflict*booleanThe saved filters changed under this draft, so Save is refused.
hasErrors*booleanSome error shows, so the panel says the filters can't be applied.
canAdd*booleanFewer filters than the limit, so Add Filter can add one.
add*() => number | nullAppends a blank new filter and returns its key, or null at the limit.
replace*(key: number, condition: ListViewFilterCondition) => voidReplaces a filter's condition, marking it changed when it differs.
remove*(key: number) => voidRemoves a filter, renumbering the rest and rewriting the logic.
removeAll*() => voidRemoves every editable filter and turns logic off.
setOwner*(owner: "all" | "mine") => voidSets the owner scope in the draft.
addLogic*() => voidTurns logic on, with every filter ANDed.
removeLogic*() => voidTurns logic off.
typeLogic*(logic: string) => voidChanges the logic text without checking it.
checkLogic*() => voidChecks the logic as it is now.
save*() => ListViewFiltersSaveResultDrops blank filters, then saves the draft unless it's invalid or the saved filters changed under it.
cancel*() => voidDrops 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:delete and utility: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-80 against 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 a relative wrapper beside the table and give it absolute 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 undefined for a field the app didn't read and null for a blank one, in the schema's read. Unread values never match, so the result is partial instead 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 useListViewFilters where it stays mounted while the panel is closed, as ListViewFilters does, or closing loses the draft.
  • Never pass asChild or render.