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!

Overview

The multi-select field family includes two components for different use cases:
  • MultiSelectField — For async data loading with support for pagination (page-based, offset-based, or group-based lazy loading)
    • Includes automatic debouncing of the search input (configurable via the debounceMs prop)
    • Includes automatic caching of the search input (configurable via the cache prop)
  • MultiSelectFieldSync — For client-side filtering of static option arrays
Both components provide a searchable dropdown interface for selecting multiple options, displaying selected values as chips with adaptive display modes (popover or dialog).

MultiSelectFieldSync (Static Options)

Use MultiSelectFieldSync when you have a static list of options that can be filtered client-side.
import { useState } from "react";
import { MultiSelectFieldSync } from "@servicetitan/anvil2/beta";

const options = [
  { id: 1, label: "Option One" },
  { id: 2, label: "Option Two" },
  { id: 3, label: "Option Three" },
];

const ExampleComponent = () => {
  const [selectedOptions, setSelectedOptions] = useState([]);

  return (
    <MultiSelectFieldSync
      label="Select options"
      placeholder="Search options..."
      options={options}
      value={selectedOptions}
      onSelectedOptionsChange={setSelectedOptions}
    />
  );
};

Custom Filtering

By default, MultiSelectFieldSync uses match-sorter to filter options by their label and searchText fields. You can customize filtering in two ways:

Using match-sorter options

<MultiSelectFieldSync
  options={options}
  filter={{ keys: ["label", "searchText"] }}
  // ...other props
/>

Using a custom filter function

<MultiSelectFieldSync
  options={options}
  filter={(options, searchValue) => {
    return options.filter((option) =>
      option.label?.toLowerCase().includes(searchValue.toLowerCase())
    );
  }}
  // ...other props
/>

MultiSelectField (Async Loading)

Use MultiSelectField when options need to be fetched from an API or when dealing with large datasets that require server-side filtering.

Basic Usage

import { useState } from "react";
import { MultiSelectField } from "@servicetitan/anvil2/beta";

const ExampleComponent = () => {
  const [selectedOptions, setSelectedOptions] = useState([]);

  return (
    <MultiSelectField
      label="Select Tags"
      placeholder="Search tags..."
      loadOptions={async (searchValue) => {
        const response = await fetch(`/api/tags?q=${searchValue}`);
        const tags = await response.json();
        return tags.map((tag) => ({
          id: tag.id,
          label: tag.name,
        }));
      }}
      value={selectedOptions}
      onSelectedOptionsChange={setSelectedOptions}
    />
  );
};

Lazy Loading Modes

MultiSelectField supports three lazy loading modes for paginated data:

Page-based Pagination

<MultiSelectField
  lazy="page"
  lazyOptions={{ pageSize: 10 }}
  loadOptions={async (searchValue, pageNumber, pageSize) => {
    const response = await fetch(
      `/api/items?q=${searchValue}&page=${pageNumber}&size=${pageSize}`
    );
    const { data, totalCount } = await response.json();
    return {
      options: data.map((item) => ({ id: item.id, label: item.name })),
      hasMore: pageNumber * pageSize + data.length < totalCount,
    };
  }}
  // ...other props
/>

Offset-based Pagination

<MultiSelectField
  lazy="offset"
  lazyOptions={{ limit: 15 }}
  loadOptions={async (searchValue, offset, limit) => {
    const response = await fetch(
      `/api/items?q=${searchValue}&offset=${offset}&limit=${limit}`
    );
    const { data, totalCount } = await response.json();
    return {
      options: data.map((item) => ({ id: item.id, label: item.name })),
      hasMore: offset + limit < totalCount,
    };
  }}
  // ...other props
/>

Group-based Loading

For loading grouped options incrementally:
<MultiSelectField
  lazy="group"
  groupToString={(groupValue) => String(groupValue)}
  loadOptions={async (searchValue, previousGroupKey) => {
    const response = await fetch(
      `/api/items?q=${searchValue}&afterGroup=${previousGroupKey || ""}`
    );
    const { data, hasMore } = await response.json();
    return {
      options: data.map((item) => ({
        id: item.id,
        label: item.name,
        group: item.category, // Required for grouped options
      })),
      hasMore,
    };
  }}
  // ...other props
/>

Select All

Enable bulk selection with the selectAll prop.

MultiSelectField

With MultiSelectField, the parent component is responsible for handling the select/deselect logic via onClick and managing the isChecked state:
import { useState } from "react";
import { MultiSelectField } from "@servicetitan/anvil2/beta";

const allOptions = [
  { id: 1, label: "Option One" },
  { id: 2, label: "Option Two" },
  { id: 3, label: "Option Three" },
];

const ExampleComponent = () => {
  const [selectedOptions, setSelectedOptions] = useState([]);

  const handleSelectAll = () => {
    if (selectedOptions.length === allOptions.length) {
      setSelectedOptions([]);
    } else {
      setSelectedOptions(allOptions);
    }
  };

  return (
    <MultiSelectField
      label="Select Items"
      loadOptions={async () => allOptions}
      value={selectedOptions}
      onSelectedOptionsChange={setSelectedOptions}
      selectAll={{
        label: "Select All Items",
        onClick: handleSelectAll,
        isChecked:
          selectedOptions.length === allOptions.length
            ? "checked"
            : selectedOptions.length > 0
              ? "indeterminate"
              : "unchecked",
      }}
    />
  );
};

MultiSelectFieldSync

MultiSelectFieldSync provides a simplified selectAll prop. Click handling and check state are managed automatically:
// Enable with default label
<MultiSelectFieldSync
  label="Select Tags"
  options={options}
  value={selectedOptions}
  onSelectedOptionsChange={setSelectedOptions}
  selectAll
/>

// Enable with custom label
<MultiSelectFieldSync
  label="Select Tags"
  options={options}
  value={selectedOptions}
  onSelectedOptionsChange={setSelectedOptions}
  selectAll={{ label: "Select All Tags" }}
/>

// Enable with dynamic label based on check state
<MultiSelectFieldSync
  label="Select Tags"
  options={options}
  value={selectedOptions}
  onSelectedOptionsChange={setSelectedOptions}
  selectAll={{ label: (checked) => checked ? "Deselect All" : "Select All" }}
/>

Chip Display Options

Control how selected options are displayed as chips:

Single Row Mode

Restrict the field to a single row height. Overflow chips are collapsed into a “+N” indicator:
<MultiSelectField
  label="Tags"
  loadOptions={loadOptions}
  value={selectedOptions}
  onSelectedOptionsChange={setSelectedOptions}
  singleRow
/>

Max Chips

Limit the number of visible chips regardless of row height:
<MultiSelectField
  label="Tags"
  loadOptions={loadOptions}
  value={selectedOptions}
  onSelectedOptionsChange={setSelectedOptions}
  maxChips={5}
/>

Display Modes

Control how the options menu is displayed using the displayMenuAs prop:
// Automatically choose based on device (default)
<MultiSelectField displayMenuAs="auto" {...props} />

// Always show as popover (not recommended for mobile)
<MultiSelectField displayMenuAs="popover" {...props} />

// Always show as dialog
<MultiSelectField displayMenuAs="dialog" {...props} />

Caching

MultiSelectField caches loadOptions results by default. Configure caching behavior:
// Disable caching
<MultiSelectField cache={{ enabled: false }} {...props} />

// Configure max cache size (default: 50)
<MultiSelectField cache={{ maxSize: 100 }} {...props} />

Clearing the Cache

Use a ref to imperatively clear the cache:
import { useRef } from "react";
import { MultiSelectField } from "@servicetitan/anvil2/beta";

const ExampleComponent = () => {
  const multiSelectRef = useRef(null);

  const handleRefresh = () => {
    multiSelectRef.current?.clearCache();
  };

  return (
    <>
      <MultiSelectField ref={multiSelectRef} {...props} />
      <button onClick={handleRefresh}>Refresh Options</button>
    </>
  );
};

Initial Load Behavior

Control when options are first loaded with the initialLoad prop:
// Load immediately on mount (default)
<MultiSelectField initialLoad="immediate" {...props} />

// Load when user opens the dropdown
<MultiSelectField initialLoad="open" {...props} />

// Auto (currently equivalent to "immediate")
<MultiSelectField initialLoad="auto" {...props} />

Field States

Error State

Display validation errors using the error prop:
<MultiSelectField
  label="Categories"
  loadOptions={loadOptions}
  value={selectedOptions}
  onSelectedOptionsChange={setSelectedOptions}
  error={selectedOptions.length === 0 ? "Please select at least one category" : false}
/>

Hint and Description

Provide additional context with hint and description:
<MultiSelectField
  label="Categories"
  loadOptions={loadOptions}
  value={selectedOptions}
  onSelectedOptionsChange={setSelectedOptions}
  hint="Select up to 5 categories"
  description="These will be used for filtering"
/>

Required Field

Mark a field as required with the required prop:
<MultiSelectField
  label="Categories"
  loadOptions={loadOptions}
  value={selectedOptions}
  onSelectedOptionsChange={setSelectedOptions}
  required
/>

Disabled and ReadOnly

When disabled is set, users cannot interact with the field:
<MultiSelectField
  label="Categories"
  loadOptions={loadOptions}
  value={selectedOptions}
  onSelectedOptionsChange={setSelectedOptions}
  disabled
/>
When readOnly is set, users can see the dropdown but cannot change selections:
<MultiSelectField
  label="Categories"
  loadOptions={loadOptions}
  value={selectedOptions}
  onSelectedOptionsChange={setSelectedOptions}
  readOnly
/>

Prefix and Suffix

Add content before or after the input with prefix and suffix:
<MultiSelectField
  label="Tags"
  loadOptions={loadOptions}
  value={selectedOptions}
  onSelectedOptionsChange={setSelectedOptions}
  prefix="#"
  suffix="tags"
/>

Sizes

Control the size of the field with the size prop:
<MultiSelectField size="small" {...props} />
<MultiSelectField size="medium" {...props} /> {/* default */}
<MultiSelectField size="large" {...props} />

Disabled Options

Individual options can be disabled by setting disabled: true on the option:
const options = [
  { id: 1, label: "Available Option" },
  { id: 2, label: "Unavailable Option", disabled: true },
  { id: 3, label: "Another Available Option" },
];

<MultiSelectField
  label="Select options"
  loadOptions={async () => options}
  value={selectedOptions}
  onSelectedOptionsChange={setSelectedOptions}
/>
Last modified on January 23, 2026