Skip to content

Filter

Shows one filter criterion that opens an editor when pressed. For a list view's whole filter panel, use List view filter panel.

Installation

pnpm dlx shadcn@latest add @wisty/filter

The CLI installs the Radix or Base UI file to match your project, with what it builds on: button, popover.

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

  • To show one filter criterion, like Account Name contains "Acme", as an item people press to edit it in a popover.
  • When the criterion can be new, changed but not saved, invalid or locked, and people need to see which.
  • In a filter panel, a report's filters or a builder of conditions, where the page owns the list and the editor's fields.

Don't use it

  • For a list view's whole filter panel, with Filter by Owner, filter logic and Save. Use List view filter panel, which is built from these parts.
  • For a quick choice that filters as soon as it changes, like a status picker above a table. Use Select or Toggle group.
  • To search rows by text. Use an Input or List view header's search.

Usage

Filter
├── FilterMarker
├── FilterItem
│   ├── FilterTrigger
│   │   ├── FilterProperty
│   │   │   └── FilterStatus
│   │   └── FilterPredicate
│   └── FilterRemove
├── FilterError
└── FilterContent
import {
  Filter,
  FilterContent,
  FilterError,
  FilterItem,
  FilterPredicate,
  FilterProperty,
  FilterRemove,
  FilterStatus,
  FilterTrigger,
} from "@/components/wisty/filter"
<Filter status="changed" invalid={!!error} open={open} onOpenChange={setOpen}>
  <FilterItem>
    <FilterTrigger>
      <FilterProperty>
        Amount
        <FilterStatus />
      </FilterProperty>
      <FilterPredicate>greater than $50,000</FilterPredicate>
    </FilterTrigger>
    <FilterRemove aria-label="Remove filter: Amount greater than $50,000" />
  </FilterItem>
  <FilterError>{error}</FilterError>
  <FilterContent aria-label="Choose filter criteria">{editor}</FilterContent>
</Filter>

FilterContent holds whatever edits the criterion: fields and the buttons that apply or drop them. It adds no buttons of its own. Close it by setting open to false from those buttons. FilterMarker holds a number such as 1. beside the item, for filters that filter logic refers to by position. FilterStatus draws the asterisk after the label of a new or changed criterion. Write it where the asterisk belongs, or leave it out, as Filter by Owner does.

Examples

New, changed and in error

Numbered criteria with FilterMarker: one new, one changed with its FilterStatus asterisk, and one invalid with its FilterError.

  1. Region isn't available to filter by.

Locked with a reason

A locked criterion people can read but not open or remove, with its reason written under it.

This view shows open cases only.

API reference

PartBuilt onDescription
Filter<div> (opens in a new tab)One criterion people press to edit: its item, an optional marker, its error and its editor.
FilterMarker<span> (opens in a new tab)
FilterItem<div> (opens in a new tab)
FilterTrigger<button> (opens in a new tab)
FilterProperty<span> (opens in a new tab)
FilterStatus<span> (opens in a new tab)
FilterPredicate<span> (opens in a new tab)
FilterRemoveButton
FilterError<p> (opens in a new tab)
FilterContentPopoverContent

Filter

One criterion people press to edit: its item, an optional marker, its error and its editor.

PropTypeDefaultDescription
statusFilterState"unchanged"Whether the criterion is saved, new or changed since it was saved. Default "unchanged".
invalidbooleanfalseThe criterion can't be applied; write its reason in FilterError.
lockedbooleanfalseShown but never edited or removed: the trigger is disabled and the editor never opens.
openbooleanWhether the editor is open, controlled.
defaultOpenbooleanWhether the editor is open at first, uncontrolled.
onOpenChange(open: boolean) => voidCalled when the editor opens or closes.

FilterContent

PropTypeDefault
align"center" | "end" | "start""end"
side"top" | "bottom" | "left" | "right""left"
sideOffsetnumber10

FilterStatetype

type FilterState = "unchanged" | "new" | "changed"

SLDS mapping

Filter is a criterion's wrapper and FilterItem its slds-filters__item, with slds-is-new for status new or changed, slds-has-error for invalid and slds-is-locked for locked. FilterTrigger is the item's slds-button_reset holding the slds-text-body_small label (FilterProperty) and the value (FilterPredicate). FilterRemove is the small icon button. FilterError is the slds-text-color_error message under the item. FilterContent is slds-popover slds-popover_large opening on the left with slds-nubbin_right-bottom.

Known gaps:

  • FilterRemove uses lucide's trash glyph where SLDS draws utility:delete.
  • SLDS's grouped filters (slds-filters__group) and the expression builder's rows aren't parts here.
PropValueSLDSNotes
statusunchanged (added)slds-filters__item
statusnew (added)slds-filters__item slds-is-new
statuschanged (added)slds-filters__item slds-is-new
invalidtrue (added)slds-filters__item slds-has-error
lockedtrue (added)slds-filters__item slds-is-locked

Accessibility

  • The trigger's name is its text, so write the words a screen reader should say first, like an sr-only "Edit filter 1:", inside FilterTrigger. A new or changed criterion adds "Not saved" or "Changed, not saved" at the end.
  • Give FilterRemove an aria-label that names the criterion, and FilterContent an aria-label or aria-labelledby.
  • invalid describes the trigger with FilterError's text, so write the error whenever invalid is true.
  • A locked trigger is disabled, so write the reason as visible text beside it.

Rules

  • Pass status, invalid and locked from the data the page holds. The filter never works out whether a criterion changed or can be applied.
  • A locked filter never opens its editor or shows FilterRemove. Enforce what it stands for where the data is read.
  • Never pass asChild or render.