> ## 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.

# Filter Bar – Code

> Standalone filter bar component with filtering controls

export const LiveCode = ({children, customHeight, clickToLoad, example, fullWidth, fullHeight, hideCodeInLiveCode, screenshot, screenshotOnly, showCode: showCodeProp}) => {
  const SCREENSHOTS_BASE = "https://servicetitan.github.io/anvil2-docs-live-code/screenshots";
  const STACKBLITZ_BASE = "https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples";
  const [showCodeBlock, setShowCodeBlock] = useState(showCodeProp ?? false);
  const [isLocalOverride, setIsLocalOverride] = useState(false);
  useEffect(() => {
    const examplePath = `/images/live-code-screenshots-tmp/${example}.png`;
    fetch(examplePath, {
      method: "HEAD"
    }).then(r => {
      if (r.ok) setIsLocalOverride(true);
    }).catch(() => {});
  }, [example]);
  const screenshotBase = isLocalOverride ? "/images/live-code-screenshots-tmp" : SCREENSHOTS_BASE;
  if (screenshotOnly) {
    return <Frame className="flex flex-col">
        <div className="flex dark:hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#FFFFFF"
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>
        <div className="hidden dark:flex" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#141414"
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>
      </Frame>;
  }
  if (screenshot) {
    return <Frame className="flex flex-col -mb-2">
        <div className="flex dark:hidden bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>

        <div className="hidden dark:flex bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      background: "#141414",
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>

        <div className="flex justify-end items-center text-xs py-2 px-1 gap-4">
          {!showCodeProp ? <button className="inline-flex justify-end items-center text-gray-700 dark:text-gray-50 hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1 cursor-pointer" onClick={() => setShowCodeBlock(!showCodeBlock)} style={{
      appearance: "none"
    }}>
              <Icon icon="code" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
              <span>{showCodeBlock ? "Hide code" : "Show code"}</span>
            </button> : null}

          <a className="inline-flex justify-end items-center hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1" href={`${STACKBLITZ_BASE}/${example}?file=src/App.tsx`} target="_blank" rel="noreferrer">
            <Icon icon="bolt" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
            <span>StackBlitz demo</span>
          </a>
        </div>

        <div className="grid transition-[grid-template-rows] duration-300 ease-in-out overflow-auto overflow-y-hidden overflow-x-auto" style={showCodeBlock ? {
      gridTemplateRows: "1fr"
    } : {
      gridTemplateRows: "0fr"
    }}>
          <div style={{
      minHeight: 0,
      overflowX: "auto",
      overflowY: "hidden",
      marginBlockStart: "-1.25rem",
      marginBlockEnd: "-1.5rem"
    }}>
            {children}
          </div>
        </div>
      </Frame>;
  } else {
    return <div style={{
      display: "flex",
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px",
      resize: "vertical",
      overflow: "auto"
    }}>
        <iframe title={example} style={{
      flex: 1,
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px"
    }} src={`${STACKBLITZ_BASE}/${example}?embed=1&hideNavigation=1&hideExplorer=1&terminalHeight=0&file=src/App.tsx${clickToLoad ? "&ctl=1" : ""}${hideCodeInLiveCode ? "&view=preview" : ""}`} allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" />
      </div>;
  }
};

<Tabs>
  <Tab title="Implementation">
    <Note>
      **Beta Feature**

      This 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](https://servicetitan.enterprise.slack.com/archives/CBSRGHTRS) channel with any questions or feedback!
    </Note>

    ## Common Examples

    ### Standard Filters

    ```tsx theme={null}
    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

    ```tsx theme={null}
    {
      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.

    ```tsx theme={null}
    {
      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.

    ```tsx theme={null}
    {
      type: "multiSelect",
      id: "multiSelectFilterId",
      label: "Categories",
      options: [
        { id: "bug", label: "Bug" },
        { id: "feature", label: "Feature" },
      ],
      selectedOptions: [],
      selectAll: true,
      selectFiltered: true,
    }
    ```

    #### Single Date Selection

    ```tsx theme={null}
    {
      type: "date",
      id: "dateFilterId",
      label: "Date Filter",
      mode: "mm/dd/yyyy",
    }
    ```

    #### Range Date Selection

    ```tsx theme={null}
    {
      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.

    ```tsx theme={null}
    {
      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.

    ```tsx theme={null}
    {
      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.

    ```tsx theme={null}
    {
      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.

    ```tsx theme={null}
    {
      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.

    ```tsx theme={null}
    {
      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

    ```tsx theme={null}
    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

    ```tsx theme={null}
    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

    ```tsx theme={null}
    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

    ```tsx theme={null}
    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

    ```tsx theme={null}
    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](/docs/web/components/toolbar/design) control options to have additional actions. The `FilterBar` and `Toolbar` are rendered as siblings and can be composed with `Flex` for layout.

    ### Responsive Behavior

    ```tsx theme={null}
    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 {associatedContent}".
  </Tab>

  <Tab title="FilterBar Props">
    ```tsx theme={null}
    import { FilterBar } from "@servicetitan/anvil2/beta";

    function ExampleComponent() {
    return (

    <FilterBar
      associatedContent="..."
      filters={filters}
      controlledFiltering={false}
      onFilterChange={(filters) => console.log(filters)}
    />
    ); }

    ```

    ## `FilterBar` Props

    <ParamField path="associatedContent" type="string" required>
      Describes the content being filtered for accessibility context. Used to generate aria-labels.
    </ParamField>

    <ParamField path="filters" type="Filter[]" required>
      Union type of all available filter objects.
    </ParamField>

    <ParamField path="onFilterChange" type="(filters: Filter[]) => void" required>
      Callback function for when filters change.
    </ParamField>

    <ParamField path="controlledFiltering" type="boolean" default="false">
      When enabled, filter button submission is controlled via an apply button.
    </ParamField>

    <ParamField path="disableCollapse" type="boolean" default="false">
      When true, filters stay inline at every container width instead of collapsing to drawer-only mode below 640px.
    </ParamField>

    <ParamField path="size" type={`"xsmall" | "small" | "medium" | "large"`} default="xsmall">
      Controls the size of the filter bar and its filter buttons.
    </ParamField>
  </Tab>

  <Tab title="Boolean Filter Props">
    ```tsx theme={null}
    {
      type: "boolean",
      id: "booleanFilterId",
      label: "Boolean Filter",
      checked: false,
    }
    ```

    ## Boolean Filter Props

    To have a boolean filter, you must have `type: "boolean"` in the filter object in the array.

    <ParamField path="type" type="boolean">
      To have a boolean filter, this type must be in the object.
    </ParamField>

    <ParamField path="checked" type="boolean" default="false">
      Selected state of the filter
    </ParamField>

    <ParamField path="id" type="string">
      Unique identifier for filter
    </ParamField>

    <ParamField path="label" type="string">
      Label for filter
    </ParamField>
  </Tab>

  <Tab title="Single Select Filter Props">
    ```tsx theme={null}
    {
      type: "singleSelect",
      id: "singleSelectFilterId",
      label: "Status",
      options: [{ id: "active", label: "Active" }],
    }
    ```

    ## Single Select Filter Props

    To have a single select filter, you must have `type: "singleSelect"` in the filter object in the array. The filter renders `SelectMenuSync` in the toolbar and `SelectFieldSync` in the drawer; client-side filtering happens automatically via match-sorter. The filter accepts the full `SelectMenuSyncProps` shape — every optional configuration (`filter`, `groupSorter`, `groupToString`, `pinned`, `virtualize`, `disableSearch`, `displayMenuAs`, `popoverWidth`, `searchPlaceholder`, `addItemLabel`, `onAddNewItem`) is supported.

    <ParamField path="id" type="string" required>
      Unique identifier for the filter
    </ParamField>

    <ParamField path="label" type="string" required>
      Display label for the filter
    </ParamField>

    <ParamField path="options" type="SelectMenuOption[]" required>
      Array of options to select from. Filtered client-side via match-sorter.
    </ParamField>

    <ParamField path="type" type={`"singleSelect"`} required>
      Identifies this as a single select filter
    </ParamField>

    <ParamField path="filter" type="SyncFilterFn | MatchSorterOptions<SelectMenuOption>">
      Custom filter behavior — either a function that returns options in the desired display order, or a `MatchSorterOptions` config. Defaults to match-sorter over `label` and `searchText`.
    </ParamField>

    <ParamField path="selectedOption" type="SelectMenuOption">
      Currently selected option.
    </ParamField>

    <ParamField path="simpleDrawerVariant" type="boolean" default="false">
      When true, the drawer cell renders a `Radio.Group` over the full option set instead of `SelectFieldSync`. The toolbar still uses `SelectMenuSync` regardless.
    </ParamField>

    <ParamField path="drawerOnly" type="boolean" default="false">
      When true, the filter is only reachable via the drawer and never rendered inline in the toolbar. Available on every filter type.
    </ParamField>
  </Tab>

  <Tab title="Multi Select Filter Props">
    ```tsx theme={null}
    {
      type: "multiSelect",
      id: "multiSelectFilterId",
      label: "Categories",
      options: [{ id: "bug", label: "Bug" }],
      selectedOptions: [],
      selectAll: true,
      selectFiltered: true,
    }
    ```

    ## Multi Select Filter Props

    To have a multi select filter, you must have `type: "multiSelect"` in the filter object in the array. The filter renders `MultiSelectMenuSync` in the toolbar and `MultiSelectFieldSync` in the drawer. Client-side filtering and the `selectAll` / `selectFiltered` affordances are managed automatically.

    <ParamField path="id" type="string" required>
      Unique identifier for the filter
    </ParamField>

    <ParamField path="label" type="string" required>
      Display label for the filter
    </ParamField>

    <ParamField path="options" type="MultiSelectMenuOption[]" required>
      Array of options to select from. Filtered client-side via match-sorter.
    </ParamField>

    <ParamField path="type" type={`"multiSelect"`} required>
      Identifies this as a multi select filter
    </ParamField>

    <ParamField path="filter" type="SyncFilterFn | MatchSorterOptions<MultiSelectMenuOption>">
      Custom filter behavior — either a function that returns options in the desired display order, or a `MatchSorterOptions` config. Defaults to match-sorter over `label` and `searchText`.
    </ParamField>

    <ParamField path="selectedOptions" type="MultiSelectMenuOption[]" default="[]">
      Currently selected options array.
    </ParamField>

    <ParamField path="selectAll" type={`boolean | { label?: string | ((checked: boolean) => string) }`}>
      Enables the "Select All" option at the top of the list (shown when search is empty). Click handling and check state are managed automatically. Pass `{ label }` to customize the label.
    </ParamField>

    <ParamField path="selectFiltered" type={`boolean | ((searchValue: string) => { label?: string })`}>
      Enables the "Select Filtered" option when a search term is active (replaces `selectAll` while searching). Click handling and check state are managed automatically.
    </ParamField>

    <ParamField path="simpleDrawerVariant" type="boolean" default="false">
      When true, the drawer cell renders a `Checkbox.Group` over the full option set instead of `MultiSelectFieldSync`. The toolbar still uses `MultiSelectMenuSync` regardless.
    </ParamField>

    <ParamField path="drawerOnly" type="boolean" default="false">
      When true, the filter is only reachable via the drawer and never rendered inline in the toolbar. Available on every filter type.
    </ParamField>
  </Tab>

  <Tab title="Single Date Selection Filter Props">
    ```tsx theme={null}
    {
      type: "date",
      id: "dateFilterId",
      label: "Date Filter",
      mode: "mm/dd/yyyy",
      value: null,
    }
    ```

    ## Single Date Selection Filter Props

    To have a single date selection filter, you must have `type: "date"` in the filter object in the array.

    <ParamField path="type" type="date">
      Identifies this as a single date filter
    </ParamField>

    <ParamField path="id" type="string">
      Unique identifier for the filter
    </ParamField>

    <ParamField path="label" type="string">
      Display label for the filter
    </ParamField>

    <ParamField path="mode" type={`"mm/dd/yyyy" | "dd/mm/yyyy"`} default="mm/dd/yyyy">
      Date format for the input.
    </ParamField>

    <ParamField path="value" type="Date | null" default="null">
      Currently selected date value.
    </ParamField>
  </Tab>

  <Tab title="Ranged Date Selection Filter Props">
    ```tsx theme={null}
    {
      type: "dateRange",
      id: "dateRangeFilterId",
      label: "Date Range Filter",
      mode: "mm/dd/yyyy",
      value: { start: null, end: null },
    }
    ```

    ## Ranged Date Selection Filter Props

    To have a ranged date selection filter, you must have `type: "dateRange"` in the filter object in the array.

    <ParamField path="type" type="dateRange">
      Identifies this as a date range filter
    </ParamField>

    <ParamField path="id" type="string">
      Unique identifier for the filter
    </ParamField>

    <ParamField path="label" type="string">
      Display label for the filter
    </ParamField>

    <ParamField path="mode" type={`"mm/dd/yyyy" | "dd/mm/yyyy"`} default="mm/dd/yyyy">
      Date format for the inputs.
    </ParamField>

    <ParamField path="value" type="{ start: Date | null; end: Date | null }" default="{ start: null, end: null }">
      Currently selected date range.
    </ParamField>
  </Tab>

  <Tab title="Date List Filter Props">
    ```tsx theme={null}
    {
      type: "dateList",
      id: "dueDateFilterId",
      label: "Due date",
      mode: "mm/dd/yyyy",
      options: [
        { id: "today", label: "Today", value: "2025-08-15" },
      ],
    }
    ```

    ## Date List Filter Props

    To have a date list filter, you must have `type: "dateList"` in the filter object in the array. FilterBar automatically renders four library options (`On…`, `Before…`, `After…`, `Custom Range…`) below the consumer-supplied options; each opens a Dialog with the appropriate date picker.

    <ParamField path="id" type="string" required>
      Unique identifier for the filter
    </ParamField>

    <ParamField path="label" type="string" required>
      Display label for the filter
    </ParamField>

    <ParamField path="options" type="DateListOption[]" required>
      Consumer-supplied options. Each option commits its `value` immediately on click. The reserved ids `on`, `before`, `after`, and `customRange` are used by the library and must not appear in consumer options.
    </ParamField>

    <ParamField path="type" type={`"dateList"`} required>
      Identifies this as a date list filter
    </ParamField>

    <ParamField path="drawerOnly" type="boolean" default="false">
      When true, the filter is only reachable via the drawer and never rendered inline in the toolbar. Available on every filter type.
    </ParamField>

    <ParamField path="mode" type={`"mm/dd/yyyy" | "dd/mm/yyyy"`} default="mm/dd/yyyy">
      Date format for the date pickers.
    </ParamField>

    <ParamField path="selectedOption" type="DateListOption">
      The currently selected option (consumer or library).
    </ParamField>
  </Tab>

  <Tab title="Date List Option">
    ```tsx theme={null}
    {
      id: "today",
      label: "Today",
      value: "2025-08-15",
    }
    ```

    ## Date List Option

    Each entry in a date list filter's `options` array has this shape. An option with `value: null` is treated as no filter applied; selecting it clears the filter.

    <ParamField path="id" type="string" required>
      Unique identifier for the option. The reserved ids `on`, `before`, `after`, and `customRange` must not be used.
    </ParamField>

    <ParamField path="label" type="string" required>
      Display label for the option.
    </ParamField>

    <ParamField path="value" type={`string | { startDate: string; endDate: string } | null`} required>
      Concrete value the option resolves to: an ISO date string, a date range, or `null` to represent "no filter applied".
    </ParamField>
  </Tab>

  <Tab title="Async Select Filter Props">
    ```tsx theme={null}
    {
      type: "asyncSelect",
      id: "ownerFilterId",
      label: "Owner",
      loadOptions: async (searchValue) => fetchOwners(searchValue),
    }
    ```

    ## Async Select Filter Props

    To have an async select filter, you must have `type: "asyncSelect"` in the filter object in the array. The filter renders `SelectMenu` in the toolbar and `SelectField` in the drawer; both share the same `loadOptions` configuration. The filter accepts the full `SelectMenuProps` union — every option-loading mode (eager + lazy `page` / `offset` / `group`) and every optional configuration (`pinned`, `cache`, `virtualize`, `displayMenuAs`, `disableSearch`, `popoverWidth`, `groupToString`, `groupSorter`, `searchPlaceholder`, `debounceMs`, `initialLoad`, `addItemLabel`, `onAddNewItem`) is supported.

    <ParamField path="id" type="string" required>
      Unique identifier for the filter
    </ParamField>

    <ParamField path="label" type="string" required>
      Display label for the filter
    </ParamField>

    <ParamField path="loadOptions" type="(searchValue, ...) => SelectFieldOption[] | Promise<...>" required>
      Function that returns the options for the current search value. Shape varies with the `lazy` mode — see `SelectMenu` for the full loader signatures.
    </ParamField>

    <ParamField path="type" type={`"asyncSelect"`} required>
      Identifies this as an async select filter
    </ParamField>

    <ParamField path="lazy" type={`"page" | "offset" | "group" | false`} default="false">
      Lazy-loading mode. When set, `loadOptions` is called incrementally as the user scrolls; when omitted, all options are loaded eagerly per search.
    </ParamField>

    <ParamField path="selectedOption" type="SelectMenuOption">
      The currently selected option.
    </ParamField>

    <ParamField path="drawerOnly" type="boolean" default="false">
      When true, the filter is only reachable via the drawer and never rendered inline in the toolbar. Available on every filter type.
    </ParamField>
  </Tab>

  <Tab title="Async Multi Select Filter Props">
    ```tsx theme={null}
    {
      type: "asyncMultiSelect",
      id: "reviewersFilterId",
      label: "Reviewers",
      selectedOptions: [],
      loadOptions: async (searchValue) => fetchReviewers(searchValue),
    }
    ```

    ## Async Multi Select Filter Props

    To have an async multi-select filter, you must have `type: "asyncMultiSelect"` in the filter object in the array. The filter renders `MultiSelectMenu` in the toolbar and `MultiSelectField` in the drawer; both share the same `loadOptions` configuration. The filter accepts the full `MultiSelectMenuProps` union — every option-loading mode (eager + lazy `page` / `offset` / `group`) and every optional configuration (`pinned`, `cache`, `virtualize`, `displayMenuAs`, `disableSearch`, `popoverWidth`, `groupToString`, `groupSorter`, `searchPlaceholder`, `debounceMs`, `initialLoad`, `selectAll`, `selectFiltered`, `addItemLabel`, `onAddNewItem`) is supported.

    <ParamField path="id" type="string" required>
      Unique identifier for the filter
    </ParamField>

    <ParamField path="label" type="string" required>
      Display label for the filter
    </ParamField>

    <ParamField path="loadOptions" type="(searchValue, ...) => MultiSelectFieldOption[] | Promise<...>" required>
      Function that returns the options for the current search value. Shape varies with the `lazy` mode — see `MultiSelectMenu` for the full loader signatures.
    </ParamField>

    <ParamField path="type" type={`"asyncMultiSelect"`} required>
      Identifies this as an async multi-select filter
    </ParamField>

    <ParamField path="lazy" type={`"page" | "offset" | "group" | false`} default="false">
      Lazy-loading mode. When set, `loadOptions` is called incrementally as the user scrolls; when omitted, all options are loaded eagerly per search.
    </ParamField>

    <ParamField path="selectedOptions" type="MultiSelectMenuOption[]" default="[]">
      The currently selected options.
    </ParamField>

    <ParamField path="selectAll" type="BulkActionConfig">
      Configuration for the "Select All" checkbox shown above the option list when the search input is empty. Pass exactly one of `onClick` (consumer commits via `onSelectedOptionsChange`) or `compute(current) => next | Promise<next>` (the menu/field commits the result and shows a loading state while the promise resolves). `checkState` should be derived from the current selection.
    </ParamField>

    <ParamField path="selectFiltered" type="(searchValue: string) => BulkActionConfig">
      Configuration for the "Select Filtered" checkbox shown above the option list when a search term is active (replaces `selectAll` while searching). Called with the current search value so the consumer can scope the action to matching results. Accepts the same `onClick` / `compute` choice as `selectAll`.
    </ParamField>

    <ParamField path="drawerOnly" type="boolean" default="false">
      When true, the filter is only reachable via the drawer and never rendered inline in the toolbar. Available on every filter type.
    </ParamField>
  </Tab>

  <Tab title="Tree Filter Props">
    ```tsx theme={null}
    {
      type: "tree",
      id: "departmentFilterId",
      label: "Department",
      options: [
        {
          id: "eng",
          label: "Engineering",
          children: [{ id: "fe", label: "Frontend" }],
        },
      ],
      selectionMode: "linked",
      valueConsistsOf: "LEAF_PRIORITY",
      defaultExpandLevel: 1,
    }
    ```

    ## Tree Filter Props

    To have a tree filter, you must have `type: "tree"` in the filter object in the array. The filter renders `TreeSelectMenuSync` in the toolbar and `TreeSelectFieldSync` in the drawer over a static `options` tree; client-side search happens automatically (parents of matches are preserved). The filter accepts the full `TreeSelectMenuSyncProps` shape — every optional configuration (`filter`, `selectionMode`, `valueConsistsOf`, `defaultExpandLevel`, `expandedIds`, `virtualize`, `disableSearch`, `displayMenuAs`, `popoverWidth`, `searchPlaceholder`) is supported.

    <ParamField path="id" type="string" required>
      Unique identifier for the filter
    </ParamField>

    <ParamField path="label" type="string" required>
      Display label for the filter
    </ParamField>

    <ParamField path="options" type="TreeSelectFieldNode[]" required>
      The static tree to display. Each node has `id`, `label`, and optional `children`, `disabled`, `searchText`, `childCount`, and `content`.
    </ParamField>

    <ParamField path="type" type={`"tree"`} required>
      Identifies this as a tree filter
    </ParamField>

    <ParamField path="selectedNodes" type="TreeSelectMenuValue[]" default="[]">
      The currently selected node values. Always an array, even in `selectionMode: "single"`.
    </ParamField>

    <ParamField path="selectionMode" type={`"single" | "independent" | "linked"`} default={`"linked"`}>
      "single" selects one node at a time. "independent" toggles nodes without cascading. "linked" cascades selection between parents and children.
    </ParamField>

    <ParamField path="valueConsistsOf" type={`"ALL" | "BRANCH_PRIORITY" | "BRANCH_ONLY" | "LEAF_PRIORITY" | "LEAF_ONLY"`} default={`"LEAF_PRIORITY"`}>
      Controls which nodes are selectable and how the value array is shaped.
    </ParamField>

    <ParamField path="drawerOnly" type="boolean" default="false">
      When true, the filter is only reachable via the drawer and never rendered inline in the toolbar. Available on every filter type.
    </ParamField>
  </Tab>

  <Tab title="Async Tree Filter Props">
    ```tsx theme={null}
    {
      type: "asyncTree",
      id: "orgChartFilterId",
      label: "Org Chart",
      loadOptions: async (searchValue, parentNode) =>
        fetchOrgChart(searchValue, parentNode),
      selectionMode: "linked",
      valueConsistsOf: "LEAF_PRIORITY",
      initialLoad: "open",
    }
    ```

    ## Async Tree Filter Props

    To have an async tree filter, you must have `type: "asyncTree"` in the filter object in the array. The filter renders `TreeSelectMenu` in the toolbar and `TreeSelectField` in the drawer; both share the same `loadOptions` configuration, which is called for the root nodes and again with a `parentNode` for lazy branch expansion. The filter accepts the full `TreeSelectMenuProps` shape — every optional configuration (`cache`, `virtualize`, `displayMenuAs`, `disableSearch`, `popoverWidth`, `searchPlaceholder`, `debounceMs`, `initialLoad`, `selectionMode`, `valueConsistsOf`, `defaultExpandLevel`, `expandedIds`) is supported.

    <ParamField path="id" type="string" required>
      Unique identifier for the filter
    </ParamField>

    <ParamField path="label" type="string" required>
      Display label for the filter
    </ParamField>

    <ParamField path="loadOptions" type="(searchValue, parentNode?) => TreeSelectFieldNode[] | Promise<...>" required>
      Function that loads tree nodes. Called for the root nodes, and again with a `parentNode` to lazily load a branch's children.
    </ParamField>

    <ParamField path="type" type={`"asyncTree"`} required>
      Identifies this as an async tree filter
    </ParamField>

    <ParamField path="selectedNodes" type="TreeSelectMenuValue[]" default="[]">
      The currently selected node values. Always an array, even in `selectionMode: "single"`.
    </ParamField>

    <ParamField path="initialLoad" type={`"auto" | "immediate" | "open"`}>
      Controls when `loadOptions` first runs: `"immediate"` on mount, `"open"` when the menu opens, or `"auto"`.
    </ParamField>

    <ParamField path="selectionMode" type={`"single" | "independent" | "linked"`} default={`"linked"`}>
      "single" selects one node at a time. "independent" toggles nodes without cascading. "linked" cascades selection between parents and children.
    </ParamField>

    <ParamField path="valueConsistsOf" type={`"ALL" | "BRANCH_PRIORITY" | "BRANCH_ONLY" | "LEAF_PRIORITY" | "LEAF_ONLY"`} default={`"LEAF_PRIORITY"`}>
      Controls which nodes are selectable and how the value array is shaped.
    </ParamField>

    <ParamField path="drawerOnly" type="boolean" default="false">
      When true, the filter is only reachable via the drawer and never rendered inline in the toolbar. Available on every filter type.
    </ParamField>
  </Tab>

  <Tab title="Custom Filter Props">
    ```tsx theme={null}
    {
      type: "custom",
      id: "customFilterId",
      label: "Custom Filter",
      buttonRender: ({ value, onChange }) => <Component />,
      drawerRender: ({ value, onChange }) => <Component />,
      value: null,
      labelChipCount: 0,
    }
    ```

    ## Custom Filter Props

    To have a custom filter, you must have `type: "custom"` in the filter object in the array.

    <ParamField path="type" type="custom">
      Identifies this as a custom filter
    </ParamField>

    <ParamField path="id" type="string">
      Unique identifier for the filter
    </ParamField>

    <ParamField path="label" type="string">
      Display label for the filter
    </ParamField>

    <ParamField path="buttonRender" type="(props: { value?: T; onChange: (value?: T) => void }) => ReactNode">
      Function to render the filter content in the button/popover. Required unless `drawerOnly` is true, in which case it is forbidden.
    </ParamField>

    <ParamField path="drawerRender" type="(props: { value?: T; onChange: (value?: T) => void }) => ReactNode">
      Function to render the filter content in the drawer
    </ParamField>

    <ParamField path="labelChipCount" type="number">
      Optional number to display in a label chip
    </ParamField>

    <ParamField path="value" type="T">
      Current value of the custom filter
    </ParamField>

    <ParamField path="drawerOnly" type="boolean" default="false">
      When true, the filter is only reachable via the drawer and never rendered inline in the toolbar. Available on every filter type.
    </ParamField>
  </Tab>
</Tabs>
