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 { Toolbar } from "@servicetitan/anvil2/beta";

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

  return (
    <Toolbar associatedContent="...">
      <Toolbar.Filters
        filters={filters}
        onFilter={(updatedFilters) => console.log(updatedFilters)}
      />
    </Toolbar>
  );
}
A filter group is created by passing a filters object array to the Toolbar.Filters 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 currently 5 available 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.Simple Example
{
  type: "singleSelect",
  id: "singleSelectFilterId",
  label: "Single Select Filter",
  items: [
    {
      id: "singleOption1",
      label: "Single Option 1"
    },
    ...
  ],
  simpleDrawerVariant
}
With Search ExampleSince population of the item list is controlled by implementing team, the Single Select search handler needs to have an empty search term conditional in it so the filter drawer draft state can properly reset the displayed items after selection but before applying the batch update.
{
  type: "singleSelect",
  id: "singleSelectFilterId",
  label: "Single Select Filter",
  items: [
    {
      id: "singleOption1",
      label: "Single Option 1"
    },
    ...
  ],
  hasSearch: true,
  onSearch: (searchTerm) => {
    if (!searchTerm) {
      setSingleSelectOptions(statusOptions);
    } else {
      const filtered = statusOptions.filter((option) =>
        option.label.toLowerCase().includes(searchTerm.toLowerCase()));
      setSingleSelectOptions(filtered);
    }
  },
  onSearchClear: handleMultiselectSearchClear,
}

Multi 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.Simple Example
{
  type: "multiSelect",
  id: "multiSelectFilterId",
  label: "Multi Select Filter",
  items: [
    {
      id: "multiOption1",
      label: "Multi Option 1"
    },
    ...
  ],
  simpleDrawerVariant
}
With Search ExampleSince population of the item list is controlled by implementing team, the Single Select search handler needs to have an empty search term conditional in it so the filter drawer draft state can properly reset the displayed items after selection but before applying the batch update.
{
  type: "multiSelect",
  id: "multiSelectFilterId",
  label: "Multi Select Filter",
  items: [
    {
      id: "multiOption1",
      label: "Multi Option 1"
    },
    ...
  ],
  selectedItems: [],
  hasSearch: true,
  onSearch: (searchTerm) => {
    if (!searchTerm) {
      setMultiSelectOptions(statusOptions);
    } else {
      const filtered = statusOptions.filter((option) =>
        option.label.toLowerCase().includes(searchTerm.toLowerCase()));
      setMultiSelectOptions(filtered);
    }
  },,
  onSearchClear: handleMultiselectSearchClear,
}

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",
}
These filter types come with preset filter drawer variants.

Custom Filters

import { Toolbar } 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(
    <Toolbar>
      <Toolbar.Filters
        filters={filters}
        onFilter={
          (updateFilters) => {
            console.log(updateFilters);
            setLabelText(....);
          }
        }
      />
    </Toolbar>
  )
}
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.

Filters with Apply/Cancel Triggers

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

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

  return (
    <Toolbar>
      <Toolbar.Filters
        filters={filters}
        onFilter={
          (updatedFilters) => console.log(updatedFilters)
        }
        controlledFiltering
      />
    </Toolbar>
  )
}
By default, filters will update as soon as a selection is made the in the filter button. When controlledFiltering is set to true, Apply and Cancel buttons will be added to 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.

Filters with Search Field

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

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

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

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

  return (
    <Toolbar>
      <Toolbar.Search
        placeholder="Search..."
        onChange={handleToolbarSearch}
        onClear={handleToolbarSearchClear}
      />
      <Toolbar.Filters
        filters={filters}
        onFilter={
          (updatedFilters) => console.log(updatedFilters)
        }
      />
    </Toolbar>
  )
}
Add a Toolbar.Search 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 { Toolbar } from "@servicetitan/anvil2/beta";

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

  return (
    <Toolbar>
      <Toolbar.Filters
        filters={filters}
        onFilter={
          (updatedFilters) => console.log(updatedFilters)
        }
      />
      <Toolbar.ControlGroup>
        <Toolbar.Button
          icon={{ before: AttachFile }}
          aria-label="File button"
          ...
        />
        // other base toolbar controls
      </Toolbar.ControlGroup>
    </Toolbar>
  )
}
Add a Toolbar.ControlGroup with Toolbar control options to have additional actions. The two groups will share the same overflow setting that is set on the parent Toolbar. Regardless of mode, the filters will wrap or collapse completely first before the secondary group starts.

Overflow Modes

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

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

  return (
    <>
      // Default wrap mode
      <Toolbar>
        <Toolbar.Filters
          filters={filters}
          onFilter={
            (updatedFilters) => console.log(updatedFilters)
          }
        />
      </Toolbar>

      // Collapsed overflow mode
      <Toolbar overflow="collapse">
        <Toolbar.Filters
          filters={filters}
          onFilter={
            (updatedFilters) => console.log(updatedFilters)
          }
        />
      </Toolbar>
    </>
  )
}
The groups will automatically inherit the overflow mode of the parent toolbar, which by default is set to wrap. The filters group will take overflow precedence over any secondary group, meaning it will always wrap or collapse first.In collapse mode, a hidden filter does not get added to an overflow menu like the secondary toolbar group, it will only be removed from the screen. All filters will still be presented in the filter drawer in any mode.

React Accessibility

Aria Labels

Add individual aria-label’s to the Toolbar.Filters and Toolbar.ControlGroup’s for proper group identification for screen readers. Example labels could be along the lines of “Filters for ” and “Additional actions”, where associatedContent is the prop added to the parent Toolbar for a11y context.
Last modified on January 23, 2026