Skip to main content
Beta FeatureThis feature is currently in beta, and needs to be imported from @servicetitan/anvil2/beta.While we hope to minimize breaking changes, they may occur due to feedback we receive or other improvements. These will always be documented in the changelog and communicated in Slack.Please reach out in the #ask-designsystem channel with any questions or feedback!

Common Examples

Standard Filters

import { FilterBar } from "@servicetitan/anvil2/beta";

function ExampleComponent() {
  const filters = [
    // See filter objects below
  ];

  return (
    <FilterBar
      associatedContent="..."
      filters={filters}
      onFilterChange={(updatedFilters) => console.log(updatedFilters)}
    />
  );
}
A filter group is created by passing a filters object array to the FilterBar component. The component takes the object and builds the set of filters for you according to the design patterns. You can create a custom filter if needed, but there are several prebuilt filter types to pick from:

Boolean

{
  type: "boolean",
  id: "booleanFilterId",
  label: "Boolean Filter",
  checked: false,
}

Single Select

The item list for single select is populated and controlled by the implementing team. This includes any filtering that needs to be done when a search field is present.Single select filters are backed by SelectMenuSync in the toolbar and SelectFieldSync in the drawer. Pass a static options array; client-side filtering happens automatically via match-sorter. To customize filtering, pass a filter function or a MatchSorterOptions config.
{
  type: "singleSelect",
  id: "singleSelectFilterId",
  label: "Status",
  options: [
    { id: "active", label: "Active" },
    { id: "inactive", label: "Inactive" },
  ],
  // simpleDrawerVariant?: when true, the drawer cell is a Radio.Group
  // instead of SelectFieldSync. The toolbar still uses SelectMenuSync.
}

Multi Select

Multi select filters are backed by MultiSelectMenuSync (toolbar) and MultiSelectFieldSync (drawer). Same options/filter model as single select, with selectedOptions as an array. selectAll and selectFiltered are supported and managed automatically — pass true (or { label }) to enable.
{
  type: "multiSelect",
  id: "multiSelectFilterId",
  label: "Categories",
  options: [
    { id: "bug", label: "Bug" },
    { id: "feature", label: "Feature" },
  ],
  selectedOptions: [],
  selectAll: true,
  selectFiltered: true,
}

Single Date Selection

{
  type: "date",
  id: "dateFilterId",
  label: "Date Filter",
  mode: "mm/dd/yyyy",
}

Range Date Selection

{
  type: "dateRange",
  id: "dateRangeFilterId",
  label: "Date Range Filter",
  mode: "mm/dd/yyyy",
}

Date List Selection

A Date List filter renders a single-select picker of date “buckets” that resolve to a concrete date or range. Pass the buckets you want as options; FilterBar automatically renders On…, Before…, After…, and Custom Range… below them. Selecting a menu option opens a Dialog with the appropriate date picker.
{
  type: "dateList",
  id: "dueDateFilterId",
  label: "Due date",
  mode: "mm/dd/yyyy",
  options: [
    { id: "today", label: "Today", value: "2025-08-15" },
    {
      id: "last7Days",
      label: "Last 7 days",
      value: { startDate: "2025-08-09", endDate: "2025-08-15" },
    },
  ],
}
An option with value: null is treated as no filter applied — selecting it clears the filter and the trigger button shows only the filter label. The four menu option ids (on, before, after, customRange) are reserved — don’t reuse them in your own options.

Async Select Selection

An Async Select filter is a single-select filter whose options are loaded asynchronously by your loadOptions function. In the toolbar it renders as a popover-style menu backed by SelectMenu; in the drawer it renders as a labeled form field backed by SelectField. Both surfaces consume the same loadOptions configuration, including SelectMenu’s full lazy loader union (page, offset, group) and eager loading.
{
  type: "asyncSelect",
  id: "ownerFilterId",
  label: "Owner",
  loadOptions: async (searchValue) => fetchOwners(searchValue),
}
Use this for filters whose option set is too large for an in-memory singleSelect, or where the options are server-backed (typeahead lookups, dynamic catalogs, etc.). All optional SelectMenu configuration — pinned, cache, virtualize, disableSearch, displayMenuAs, groupToString, etc. — is accepted directly on the filter object.

Async Multi Select Selection

An Async Multi Select filter is the multi-selection counterpart of Async Select — backed by MultiSelectMenu in the toolbar and MultiSelectField in the drawer. Same loadOptions configuration, same lazy loader union; the committed value is an array of selected options.
{
  type: "asyncMultiSelect",
  id: "reviewersFilterId",
  label: "Reviewers",
  selectedOptions: [],
  loadOptions: async (searchValue) => fetchReviewers(searchValue),
}
The selectAll and selectFiltered props from MultiSelectMenu are supported. Their onClick handlers are consumer-owned (you decide what “select all” means against your data source), and their checkState is derived from the current selectedOptions on each render — rebuild the filter object whenever the selection changes so the checkbox state stays in sync.

Tree

A Tree filter selects nodes from a hierarchical, static options tree. In the toolbar it renders a popover-style menu backed by TreeSelectMenuSync; in the drawer it renders a labeled form field backed by TreeSelectFieldSync. Client-side search happens automatically (parents of matches are preserved); customize it with a filter function or MatchSorterOptions config.
{
  type: "tree",
  id: "departmentFilterId",
  label: "Department",
  options: [
    {
      id: "eng",
      label: "Engineering",
      children: [
        { id: "fe", label: "Frontend" },
        { id: "be", label: "Backend" },
      ],
    },
    { id: "design", label: "Design" },
  ],
  selectionMode: "linked", // "single" | "independent" | "linked"
  valueConsistsOf: "LEAF_PRIORITY",
  defaultExpandLevel: 1,
}
The committed value is stored as selectedNodes — an array of node values, even in selectionMode: "single". All optional TreeSelectMenu configuration (selectionMode, valueConsistsOf, defaultExpandLevel, expandedIds, virtualize, disableSearch, displayMenuAs, popoverWidth, searchPlaceholder) is accepted directly on the filter object.

Async Tree

An Async Tree filter is the asynchronous counterpart of Tree — backed by TreeSelectMenu in the toolbar and TreeSelectField in the drawer. Instead of a static options tree, you supply a loadOptions callback that is called for the root nodes and again with a parentNode for lazy branch expansion.
{
  type: "asyncTree",
  id: "orgChartFilterId",
  label: "Org Chart",
  loadOptions: async (searchValue, parentNode) =>
    fetchOrgChart(searchValue, parentNode),
  selectionMode: "linked",
  valueConsistsOf: "LEAF_PRIORITY",
  initialLoad: "open",
}
Use this for trees too large to load up front, or whose branches are server-backed. The committed value is stored as selectedNodes, the same shape as the sync tree filter. All optional TreeSelectMenu configuration — cache, virtualize, displayMenuAs, initialLoad, selectionMode, valueConsistsOf, defaultExpandLevel, etc. — is accepted directly on the filter object.These filter types come with preset filter drawer variants.

Custom Filters

import { FilterBar } from "@servicetitan/anvil2/beta";

function ExampleComponent() {
  const [labelText, setLabelText] = useState("");

  const buttonComponent = ({ value, onChange }) => (
    <YourComponentHere value={value} onChange={onChange} />
  ))};

  const drawerComponent = ({ value, onChange }) => (
    <YourComponentHere value={value} onChange={onChange} />
  ))};

  const filters = [
    {
      type: "custom",
      id: "customFilterId",
      label: labelText,
      buttonRender: buttonComponent,
      drawerRender: renderComponent
    }
  ];

  return(
    <FilterBar
      associatedContent="..."
      filters={filters}
      onFilterChange={
        (updateFilters) => {
          console.log(updateFilters);
          setLabelText(....);
        }
      }
    />
  )
}
Provide a buttonRender, drawerRender, and custom label to create a custom filter. Label’s for custom filters can either be a string or a string with a chip count. Custom filters can be used by themselves or as part of a combined filter array.

Drawer-only Filters

import { FilterBar } from "@servicetitan/anvil2/beta";

function ExampleComponent() {
  const filters = [
    {
      type: "multiSelect",
      id: "advancedCategoryFilter",
      label: "Advanced Categories",
      options: categoryOptions,
      selectedOptions: [],
      drawerOnly: true,
    },
    {
      type: "custom",
      id: "advancedCustomFilter",
      label: "Advanced",
      drawerOnly: true,
      drawerRender: ({ value, onChange }) => (
        <YourComponentHere value={value} onChange={onChange} />
      ),
    },
  ];

  return (
    <FilterBar
      associatedContent="..."
      filters={filters}
      onFilterChange={(updatedFilters) => console.log(updatedFilters)}
    />
  );
}
Set drawerOnly: true on any filter to keep it accessible only through the filter drawer. The filter never renders inline in the toolbar, even when there is room for it. The drawerOnly flag is available on every filter type — boolean, singleSelect, multiSelect, date, dateRange, dateList, asyncSelect, asyncMultiSelect, tree, asyncTree, numericRange, textInput, and custom.For a custom filter with drawerOnly: true, buttonRender is forbidden — only drawerRender is needed, since the filter never renders inline.The drawer’s “Filters” trigger appears whenever at least one filter is unreachable from the toolbar — that is, when a filter is drawer-only or the container is too narrow (below 640px) to show inline filters. When every filter fits inline and none are drawer-only, the trigger does not render.

Filters with Apply/Cancel Triggers

import { FilterBar } from "@servicetitan/anvil2/beta";

function ExampleComponent() {
  const filters = [
    ...
  ];

  return (
    <FilterBar
      associatedContent="..."
      filters={filters}
      onFilterChange={
        (updatedFilters) => console.log(updatedFilters)
      }
      controlledFiltering
    />
  )
}
By default, filters update as soon as a selection is made in the filter button. When controlledFiltering is set to true, Apply and Cancel buttons appear at the bottom of single select, multi select, single date selection, range date selection, and custom filter button popovers to control updating. This setting has no impact on the Filter Drawer, which always submits as a batch update upon clicking Apply.

Clear Button

Single select, multi select, async select, async multi select, date, date range, and date list filters all render a “Clear” button in their popover footer. Clicking Clear resets the filter to its empty state, fires onFilterChange, and closes the menu. The behavior is built-in — the adapter wires up the clear handler, footer layout, and close behavior, and consumers cannot opt out per filter.Footer layout adapts to what else is present:
  • Clear alone — full-width row at the bottom of the popover.
  • Clear + Apply/Cancel — Clear sits to the left of the Apply/Cancel row (multi select, async multi select, date, and date range filters when controlledFiltering is on).
Clear is also wired to any consumer-provided “Add new item” affordance: with both present, Add-new stacks full-width above the Clear / Apply-Cancel row.Clear in confirmation mode is immediate — it commits an empty value regardless of any in-progress draft. The Filter Drawer has its own “Clear filters” trigger and is unaffected by this footer change.

Filters with Search Field

import { FilterBar } from "@servicetitan/anvil2/beta";
import { Toolbar } from "@servicetitan/anvil2/beta";
import { Flex } from "@servicetitan/anvil2";

function ExampleComponent() {
  const filters = [
    ...
  ];

  const handleToolbarSearch = (e) => {
    ...
  }

  const handleToolbarSearchClear = () => {
    ...
  }

  return (
    <Flex direction="column" gap={2}>
      <Toolbar associatedContent="...">
        <Toolbar.Search
          placeholder="Search..."
          onChange={handleToolbarSearch}
          onClear={handleToolbarSearchClear}
        />
      </Toolbar>
      <FilterBar
        associatedContent="..."
        filters={filters}
        onFilterChange={
          (updatedFilters) => console.log(updatedFilters)
        }
      />
    </Flex>
  )
}
Use a Toolbar.Search alongside FilterBar to include a search input with your filters. The search bar works independently from the filters themselves and has its own search props.

Filters with Additional Toolbar Items

import { FilterBar } from "@servicetitan/anvil2/beta";
import { Toolbar } from "@servicetitan/anvil2/beta";
import { Flex } from "@servicetitan/anvil2";

function ExampleComponent() {
  const filters = [
    ...
  ];

  return (
    <Flex direction="column" gap={2}>
      <FilterBar
        associatedContent="..."
        filters={filters}
        onFilterChange={
          (updatedFilters) => console.log(updatedFilters)
        }
      />
      <Toolbar associatedContent="...">
        <Toolbar.ControlGroup>
          <Toolbar.Button
            icon={{ before: AttachFile }}
            aria-label="File button"
            ...
          />
          // other base toolbar controls
        </Toolbar.ControlGroup>
      </Toolbar>
    </Flex>
  )
}
Use FilterBar alongside a Toolbar with Toolbar control options to have additional actions. The FilterBar and Toolbar are rendered as siblings and can be composed with Flex for layout.

Responsive Behavior

import { FilterBar } from "@servicetitan/anvil2/beta";

function ExampleComponent() {
  const filters = [
    ...
  ];

  return (
    <FilterBar
      associatedContent="..."
      filters={filters}
      disableCollapse
      onFilterChange={(updatedFilters) => console.log(updatedFilters)}
    />
  )
}
FilterBar always wraps filters that don’t fit on a single line. Below a 640px container width, inline filters are hidden and only the drawer trigger is shown. Set disableCollapse to keep filters inline at every container width.

React Accessibility

Aria Labels

Add an associatedContent prop to the FilterBar for proper group identification for screen readers. The associatedContent value provides a11y context, generating labels like “Filters for ”.
Last modified on June 2, 2026