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 filters now render the platform’s existing select components instead of their own popover bodies:
  • singleSelectSelectMenuSync (toolbar) + SelectFieldSync (drawer)
  • multiSelectMultiSelectMenuSync (toolbar) + MultiSelectFieldSync (drawer)
Each filter’s prop shape is now (mostly) SelectMenuSyncProps / MultiSelectMenuSyncProps. Anything those components support — grouping, virtualization, pinned options, “Select All” / “Select Filtered”, dialog-on-mobile, rich option content — is available on the filter without per-filter glue code. See the SelectMenu and MultiSelectMenu docs for the full surface.

Migration Guide

Field renames

OldNew
itemsoptions
selectedItemselectedOption
selectedItemsselectedOptions
// Before
{
  type: "singleSelect",
  id: "status",
  label: "Status",
  items: [{ id: "active", label: "Active" }],
  selectedItem: undefined,
}

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

Search props removed

hasSearch, onSearch, onSearchClear, and searchValue are gone. SelectMenuSync ships a search field by default and filters client-side via match-sorter. Drop the callbacks.
// Before
{
  type: "multiSelect",
  id: "categories",
  items: options,
  selectedItems: [],
  hasSearch: true,
  onSearch: handleSearch,
  onSearchClear: handleClear,
}

// After
{
  type: "multiSelect",
  id: "categories",
  options: allCategories,
  selectedOptions: [],
}
To hide the search field, set disableSearch: true. To customize filtering, pass a filter function or MatchSorterOptions:
filter: (options, searchValue) =>
  options.filter((o) => o.label.toLowerCase().startsWith(searchValue.toLowerCase())),

// or
filter: { keys: ["label", "extra.email"] },

Custom option shapes (the Item generic)

The old filter was generic over a consumer-defined Item extends { id, label }. Options now use the fixed SelectMenuOption shape — stash custom domain data in extra:
// Before
items: [{ id: "alice", label: "Alice", email: "alice@example.com" }];

// After
options: [
  { id: "alice", label: "Alice", extra: { email: "alice@example.com" } },
];
Read it back via selectedOption.extra in your onFilterChange handler.

Server-backed options

If you were using onSearch to hit an API, switch to asyncSelect / asyncMultiSelect (backed by SelectMenu / MultiSelectMenu):
{
  type: "asyncSelect",
  id: "owner",
  label: "Owner",
  loadOptions: async (searchValue) => fetchOwners(searchValue),
}
The async variants support eager loading plus three lazy modes (page, offset, group).

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 };
  };
};

Breaking Changes

These changes require code updates before upgrading. The previous API is no longer supported.
  • Field renames: itemsoptions, selectedItemselectedOption, selectedItemsselectedOptions.
  • Search props removed: hasSearch, onSearch, onSearchClear, searchValue. The search field is built-in; client-side filtering uses match-sorter. Override with disableSearch or filter.
  • Item generic removed. Use SelectMenuOption and put custom data in extra.
Last modified on May 26, 2026