Skip to main content

Documentation Index

Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt

Use this file to discover all available pages before exploring further.

This guide covers changes to the beta FilterBar component. These APIs may continue to evolve before the stable release.

Overview

FilterBar’s singleSelect and multiSelect filter types have been reworked to use the platform’s existing select components rather than FilterBar’s own narrower implementation:
  • singleSelect filters now render SelectMenuSync in the toolbar and SelectFieldSync in the drawer.
  • multiSelect filters now render MultiSelectMenuSync in the toolbar and MultiSelectFieldSync in the drawer.
The filter type’s prop shape is now (mostly) SelectMenuSyncProps / MultiSelectMenuSyncProps. Any feature those components support is available on the filter.

Design Rationale

Why adopt SelectMenu / MultiSelectMenu?

Three coordinate wins:
  • One aligned API. The filter’s prop surface is now SelectMenuSyncProps / MultiSelectMenuSyncProps. Anyone familiar with the platform’s select components already knows how the filter behaves, and the documentation, types, and examples for those components apply directly. Async filters (asyncSelect / asyncMultiSelect) already adopt SelectMenu / MultiSelectMenu; the static-options variants now match their lazy siblings.
  • One implementation to maintain. Previously FilterBar carried its own popover bodies built on Combobox, Listbox, and ListView, with the toolbar and drawer using different UIs that needed separate tests and drifted independently. Bug fixes and feature work on the platform’s select components didn’t reach the filter. The filter now composes one component shared with everything else in the system.
  • Access to the full select feature set. Grouping, pinned options, virtualization, adaptive dialog-on-mobile, “Select All” / “Select Filtered”, search-text-vs-display-label distinctions, avatar / icon / chip content rows, “Add new item” affordances, customizable client-side filtering via match-sorter — none of that existed in FilterBar’s bespoke implementation. All of it is available on the filter now without per-filter glue code.

Why drop the Item generic?

The old filter types were generic over a consumer-defined Item extending { id, label }. The platform’s select components use a fixed SelectMenuOption shape:
{
  id: string | number;
  label: string;
  searchText?: string;
  group?: string | number;
  disabled?: boolean;
  extra?: Record<string, unknown>;
  content?: { title?, description?, chips?, avatar?, icon? };
}
Aligning on this shape unlocks the full feature set (search text, grouping, disabled options, rich content rows) without per-filter glue code. Consumers with custom domain data should stash it in extra and read it back from selectedOption.extra in their onFilterChange handler.

Reference

Field renames

Old fieldNew fieldNotes
itemsoptionsNow SelectMenuOption[] (single) / MultiSelectMenuOption[] (multi)
selectedItemselectedOptionsingleSelect
selectedItemsselectedOptionsmultiSelect

Removed fields

Removed fieldWhy it’s gone
hasSearchThe search field is part of SelectMenuSync’s UI and shown by default. Set disableSearch: true to hide it.
onSearchSelectMenuSync filters client-side via match-sorter. For custom behavior, pass a filter function or MatchSorterOptions.
onSearchClearNot applicable — search state lives inside SelectMenuSync.
searchValueNot applicable — search state lives inside SelectMenuSync.

Newly available capabilities

These all come from SelectMenuSync / MultiSelectMenuSync and didn’t exist on the old filter API:
PropTypeDescription
filterSyncFilterFn | MatchSorterOptions<…>Override the default client-side filter (match-sorter over label and searchText).
selectAllboolean | { label }(multiSelect) “Select All” checkbox shown when search is empty. Click and check state managed automatically.
selectFilteredboolean | ((searchValue: string) => { label })(multiSelect) “Select Filtered” checkbox shown when a search term is active. Replaces selectAll.
disableSearchbooleanHide the search field inside the popover.
pinnedSelectMenuPinnedOptionsPin options to the top of the list.
virtualizebooleanVirtualize the list for large option sets.
displayMenuAs"auto" | "popover" | "dialog"Override the adaptive popover/dialog rendering (auto = popover on desktop, dialog on mobile).
groupSorter(a, b) => numberCustom group ordering when options have group.
groupToString(group) => stringFormat group values as headers.
popoverWidth"reference" | number | stringOverride the popover width (defaults to 320; "reference" matches the trigger width).
addItemLabel + onAddNewItemRender an “Add new item” affordance below the option list.
Plus everything else on SelectMenuSyncProps / MultiSelectMenuSyncProps — see the SelectMenu and MultiSelectMenu docs for the full surface.

Option shape

type SelectMenuOption = {
  id: string | number;
  label: string;
  searchText?: string;
  group?: string | number;
  disabled?: boolean;
  extra?: Record<string, unknown>;
  content?: {
    title?: string;
    description?: string;
    chips?: Array<Pick<ChipProps, "label" | "color" | "textWrap">>;
    avatar?: Pick<AvatarProps, "name" | "status" | "color" | "image">;
    icon?: Pick<IconProps, "svg" | "color"> & { label?: string };
  };
};

Migration Guide

Basic single select

// Before
{
  type: "singleSelect",
  id: "statusFilterId",
  label: "Status",
  items: [
    { id: "active", label: "Active" },
    { id: "inactive", label: "Inactive" },
  ],
  selectedItem: undefined,
}

// After
{
  type: "singleSelect",
  id: "statusFilterId",
  label: "Status",
  options: [
    { id: "active", label: "Active" },
    { id: "inactive", label: "Inactive" },
  ],
  selectedOption: undefined,
}

Basic multi select

// Before
{
  type: "multiSelect",
  id: "categoryFilterId",
  label: "Categories",
  items: [
    { id: "bug", label: "Bug" },
    { id: "feature", label: "Feature" },
  ],
  selectedItems: [],
}

// After
{
  type: "multiSelect",
  id: "categoryFilterId",
  label: "Categories",
  options: [
    { id: "bug", label: "Bug" },
    { id: "feature", label: "Feature" },
  ],
  selectedOptions: [],
}

Filters that previously wired hasSearch

hasSearch, onSearch, onSearchClear, and searchValue are removed. SelectMenuSync includes a search field by default and filters its own options via match-sorter. Drop the callbacks and the state they fed.
// Before
const [options, setOptions] = useState(allCategories);

const handleSearch = (searchTerm: string) => {
  if (!searchTerm) {
    setOptions(allCategories);
  } else {
    setOptions(
      allCategories.filter((o) =>
        o.label.toLowerCase().includes(searchTerm.toLowerCase()),
      ),
    );
  }
};

const handleSearchClear = () => {
  setOptions(allCategories);
};

const filters = [
  {
    type: "multiSelect",
    id: "categories",
    label: "Categories",
    items: options,
    selectedItems: [],
    hasSearch: true,
    onSearch: handleSearch,
    onSearchClear: handleSearchClear,
  },
];

// After
const filters = [
  {
    type: "multiSelect",
    id: "categories",
    label: "Categories",
    options: allCategories,
    selectedOptions: [],
    // That's it — match-sorter filters the options client-side.
  },
];

Custom filtering behavior

If the default match-sorter behavior isn’t what you want, pass a filter function or a MatchSorterOptions config — the same prop SelectMenuSync accepts:
// Custom filter function
{
  type: "singleSelect",
  id: "ownersFilter",
  label: "Owner",
  options: owners,
  filter: (options, searchValue) =>
    options.filter((o) => o.label.toLowerCase().startsWith(searchValue.toLowerCase())),
}

// MatchSorterOptions config (extend default keys, change threshold, etc.)
{
  type: "singleSelect",
  id: "ownersFilter",
  label: "Owner",
  options: owners,
  filter: { keys: ["label", "extra.email"] },
}

selectAll and selectFiltered (multi-select only)

Not previously available on the old API — these are MultiSelectMenuSync features now exposed on the filter. Pass true for the default labels or { label } to customize:
{
  type: "multiSelect",
  id: "categories",
  label: "Categories",
  options: allCategories,
  selectedOptions: [],
  // Shown above the option list when search is empty
  selectAll: true,
  // Shown when a search term is active (replaces selectAll while searching)
  selectFiltered: true,
}
Click handling and tri-state check state (unchecked / indeterminate / checked) are managed automatically against the current selectedOptions.

Custom option shapes (the Item generic)

If your old filter used a custom Item shape, move the extra fields into extra:
// Before
type Owner = { id: string; label: string; email: string; team: string };

const ownerOptions: Owner[] = [
  { id: "alice", label: "Alice", email: "alice@example.com", team: "design" },
];

{
  type: "singleSelect",
  id: "owner",
  label: "Owner",
  items: ownerOptions,
  selectedItem: undefined,
}

// After
const ownerOptions = [
  {
    id: "alice",
    label: "Alice",
    extra: { email: "alice@example.com", team: "design" },
  },
];

{
  type: "singleSelect",
  id: "owner",
  label: "Owner",
  options: ownerOptions,
  selectedOption: undefined,
  // Optional: include extra fields in the default search
  filter: { keys: ["label", "extra.email", "extra.team"] },
}
Read the custom data back via selectedOption.extra in your onFilterChange handler.

Server-backed options

If you were using onSearch to fetch options from a server, switch to asyncSelect or asyncMultiSelect:
// Before (consumer-managed search hitting an API)
const handleSearch = async (searchTerm: string) => {
  const result = await fetchOwners(searchTerm);
  setOptions(result);
};

// After
{
  type: "asyncSelect",
  id: "owner",
  label: "Owner",
  loadOptions: async (searchValue) => fetchOwners(searchValue),
}
The async variants are backed by SelectMenu / MultiSelectMenu (rather than their *Sync siblings) and support eager loading plus three lazy modes (page, offset, group). See the Async Select and Async Multi Select sections in the main FilterBar docs.

Breaking Changes

These changes require code updates before upgrading. The previous API is no longer supported.
  • Field renames: itemsoptions, selectedItemselectedOption, selectedItemsselectedOptions.
  • hasSearch, onSearch, onSearchClear, searchValue removed. The search field is part of SelectMenuSync and shown by default; filtering happens client-side via match-sorter. Hide the search field with disableSearch: true or override the filter behavior with the new filter prop.
  • The Item generic is removed. Options use the SelectMenuOption shape; custom domain data goes in extra.

Why Breaking?

Beta APIs can change without a deprecation cycle. The change replaces FilterBar’s parallel select implementation with the platform’s SelectMenu family — aligning the filter API with components consumers already use elsewhere, reducing FilterBar to one shared implementation instead of three, and exposing the full select feature set (grouping, virtualization, pinned options, “Select All” / “Select Filtered”, dialog mode on mobile, rich option content, etc.) on the filter. The shape change is mechanical; the gain is significant.
Last modified on May 22, 2026