FilterMenu

FilterMenu allows a user to choose from a list of options, which can be filtered. It is a composable component which consists of a trigger (for toggling a popover), a search input (for filtering results), a listbox (to display options), and a popover (which contains the listbox when opened). In small screens (tablet and mobile), the Popover will be displayed as a Sheet.

Quick Start

Installation
npm install @adaptavant/eds-core
Import
import { FilterMenu } from '@adaptavant/eds-core';

Default

Basic example

, Select Country
const initialOptions = [
  { code: "61", id: "AUS", name: "Australia" },
  { code: "64", id: "NZL", name: "New Zealand" },
  { code: "91", id: "IND", name: "India" },
  { code: "48", id: "POL", name: "Poland" },
  { code: "44", id: "GBR", name: "Scotland (UK)" },
  { code: "1", id: "USA", name: "United States" },
];

const [selectedOption, setSelectedOption] = React.useState();

const [searchTerm, setSearchTerm] = React.useState('');

function onClear(){
  return setSearchTerm('');
}

function handleInputOnChange(event) {
  return setSearchTerm(event.target.value);
}

const filteredOptions = searchTerm === "" ? initialOptions : initialOptions.filter((country) =>
    country.name.toLowerCase().includes(searchTerm.toLowerCase())
);

function OptionLabel({ name, code }) {
  return (
    <Track as="span">
      {name} +{code}
    </Track>
  );
}

function NoResults() {
  return (
    <Text className="text-secondary text-center text-body-12 py-4">
      No matching results
    </Text>
  );
}

return (
  <Field label="Select">
    <FilterMenu>
      <FilterMenuTrigger placeholder="Select Country">
        {selectedOption ? <OptionLabel {...selectedOption} /> : null}
      </FilterMenuTrigger>
      <FilterMenuPopover>
        <FilterMenuSearchField label="Search Items">
          <FilterMenuSearchInput
            onClear={onClear}
            onChange={handleInputOnChange}
            value={searchTerm}
            placeholder="Search..."
          />
        </FilterMenuSearchField>
        <FilterMenuListbox
          noResultsFallback={<NoResults />}
          options={filteredOptions}
        >
          {(option) => (
            <FilterMenuItem
              id={option.id}
              isSelected={selectedOption?.id === option.id}
              onClick={() => {
                setSelectedOption(option);
              }}
            >
              <OptionLabel {...option} />
            </FilterMenuItem>
          )}
        </FilterMenuListbox>
      </FilterMenuPopover>
    </FilterMenu>
  </Field>
);

Size

Customize the size of the FilterMenu by using the size prop for the Field.

The size can be either large or standard with standard being the default.

, Select Country
const initialOptions = [
  { code: "61", id: "AUS", name: "Australia" },
  { code: "64", id: "NZL", name: "New Zealand" },
  { code: "91", id: "IND", name: "India" },
  { code: "48", id: "POL", name: "Poland" },
  { code: "44", id: "GBR", name: "Scotland (UK)" },
  { code: "1", id: "USA", name: "United States" },
];

const [selectedOption, setSelectedOption] = React.useState();

const [searchTerm, setSearchTerm] = React.useState('');

function onClear(){
  return setSearchTerm('');
}

function handleInputOnChange(event) {
  return setSearchTerm(event.target.value);
}

const filteredOptions = searchTerm === "" ? initialOptions : initialOptions.filter((country) =>
    country.name.toLowerCase().includes(searchTerm.toLowerCase())
);

function OptionLabel({ name, code }) {
  return (
    <Track as="span">
      {name} +{code}
    </Track>
  );
}

function NoResults() {
  return (
    <Text className="text-secondary text-center text-body-12 py-4">
      No matching results
    </Text>
  );
}

return (
  <Field label="Select" size="large">
    <FilterMenu>
      <FilterMenuTrigger placeholder="Select Country">
        {selectedOption ? <OptionLabel {...selectedOption} /> : null}
      </FilterMenuTrigger>
      <FilterMenuPopover>
        <FilterMenuSearchField label="Search Items">
          <FilterMenuSearchInput
            onClear={onClear}
            onChange={handleInputOnChange}
            value={searchTerm}
            placeholder="Search..."
          />
        </FilterMenuSearchField>
        <FilterMenuListbox
          noResultsFallback={<NoResults />}
          options={filteredOptions}
        >
          {(option) => (
            <FilterMenuItem
              id={option.id}
              isSelected={selectedOption?.id === option.id}
              onClick={() => {
                setSelectedOption(option);
              }}
            >
              <OptionLabel {...option} />
            </FilterMenuItem>
          )}
        </FilterMenuListbox>
      </FilterMenuPopover>
    </FilterMenu>
  </Field>
);

Currency Picker

Using FilterMenu's compound component API, we can easily build a CurrencyPicker component from scratch.

, Select currency
const countries = [
  { currency: 'US Dollar', currencyCode: 'USD', currencySymbol: '$', id: "ASM", name: "American Samoa" },
  { currency: 'East Caribbean dollar', currencyCode: 'XCD', currencySymbol: '$', id: "AIA", name: "Anguilla" },
  { currency: 'Australian dollar', currencyCode: 'AUD', currencySymbol: '$', id: "AUS", name: "Australia" },
  { currency: 'Central African CFA franc', currencyCode: 'XAF', currencySymbol: 'FCFA', id: "TCD", name: "Chad" },
  { currency: 'Falkland Islands pound', currencyCode: 'FKP', currencySymbol: '£', id: "FLK", name: "Falkland Islands (Malvinas)"},
  { currency: 'British pound', currencyCode: 'GBP', currencySymbol: '£', id: "GGY", name: "Guernsey"},
  { currency: 'British pound', currencyCode: 'GBP', currencySymbol: '£', id: "IMN", name: "Isle of Man"},
  { currency: 'Indian rupee', currencyCode: 'INR', currencySymbol: '', id: "IND", name: "India"},
  { currency: 'Lebanese pound', currencyCode: 'LBP', currencySymbol: '£', id: "LBN", name: "Lebanon"},
  { currency: 'New Zealand dollar', currencyCode: 'NZD', currencySymbol: '$', id: "NZL", name: "New Zealand"},
  { currency: 'United States dollar', currencyCode: 'USD', currencySymbol: '$', id: "TLS", name: "Timor-Leste"},
  { currency: 'Singapore dollar', currencyCode: 'SGD', currencySymbol: '$', id: "SGP", name: "Singapore"},
  { currency: 'United Arab Emirates dirham', currencyCode: 'AED', currencySymbol: 'إ.د', id: 'ARE', name: 'United Arab Emirates',},
  { currency: 'British pound', currencyCode: 'GBP', currencySymbol: '£', id: 'GBR', name: 'United Kingdom',},
  { currency: 'United States dollar', currencyCode: 'USD', currencySymbol: '$', id: 'USA', name: 'United States',},
];

const [selectedOption, setSelectedOption] = React.useState();

const [searchTerm, setSearchTerm] = React.useState('');

function onClear() {
  return setSearchTerm('');
}

function handleInputOnChange(event) {
  return setSearchTerm(event.target.value);
}

const filteredOptions = searchTerm === "" ? countries : countries.filter((country) => 
  Object.values(country).map((value) => 
    value.toLowerCase()
  ).join(" ").includes(searchTerm.toLowerCase())
);

function escapeRegExp(string) {
	return string.replace(/[+$]/g, '\\$&');
}

function highlightMatchedContent(text, searchTerm) {
	if (!searchTerm) return text;
	const escapedSearchTerm = escapeRegExp(searchTerm);
	const regex = new RegExp(`(${escapedSearchTerm})`, 'gi');
	const parts = text.trim().split(regex);
	return parts.map((part, index) =>
		regex.test(part) ? (
			<span className="font-stronger" key={index}>
				{part}
			</span>
		) : (
			part
		)
	);
}

function OptionLabel({ name, currencyCode, currencySymbol }) {
  return (
    <Track
      className="gap-1"
      classNames={{
        center: 'grow-0',
      }}
      railEnd={<>- {highlightMatchedContent(currencyCode, searchTerm)} {highlightMatchedContent(currencySymbol, searchTerm)}</>}
    >
      <Track classNames={{ center: 'line-clamp-1'}}>
        {highlightMatchedContent(name, searchTerm)}
      </Track>
    </Track>
  );
}

function NoResults() {
  return (
    <Text className="text-secondary text-center text-body-12 py-4">
      No matching results
    </Text>
  );
}

return (
  <Field label="Currency Picker" className="w-60">
    <FilterMenu popoverMatchReferenceWidth>
      <FilterMenuTrigger placeholder="Select currency">
        {selectedOption ? <OptionLabel {...selectedOption} /> : null}
      </FilterMenuTrigger>
      <FilterMenuPopover>
        <FilterMenuSearchField label="Search Items">
          <FilterMenuSearchInput
            onClear={onClear}
            onChange={handleInputOnChange}
            value={searchTerm}
            placeholder="Search..."
          />
        </FilterMenuSearchField>
        <FilterMenuListbox
          noResultsFallback={<NoResults />}
          options={filteredOptions}
        >
          {(option) => (
            <FilterMenuItem
              id={option.id}
              isSelected={selectedOption?.id === option.id}
              onClick={() => {
                setSelectedOption(option);
              }}
            >
              <OptionLabel {...option} />
            </FilterMenuItem>
          )}
        </FilterMenuListbox>
      </FilterMenuPopover>
    </FilterMenu>
  </Field>
);

Country Code Picker

To build a CountryCodePicker component from scratch, follow the code example below.

The following snippet uses useFilteredOptions, a custom hook that manages the search function and returns a filteredOptions array. It also utilises useDeferredValue to improve performance of expensive state updates and spreads some common functions like onClear and onChange to the FilterMenuSearchInput inside a popover via getSearchInputProps.

If you prefer to use your own custom state management, refer to the CurrencyPicker example provided above.

, Select Country
const countries = [
  { currency: 'US Dollar', currencyCode: 'USD', currencySymbol: '$', id: "ASM", name: "American Samoa", dailCode: '1684' },
  { currency: 'East Caribbean dollar', currencyCode: 'XCD', currencySymbol: '$', id: "AIA", name: "Anguilla", dailCode: '1264'},
  { currency: 'Australian dollar', currencyCode: 'AUD', currencySymbol: '$', id: "AUS", name: "Australia", dailCode: '61'},
  { currency: 'Canadian dollar', currencyCode: 'CAD', currencySymbol: '$', id: "CAN", name: "Canada", dailCode: '1'},
  { currency: 'Central African CFA franc', currencyCode: 'XAF', currencySymbol: 'FCFA', id: "TCD", name: "Chad", dailCode: '235'},
  { currency: 'Falkland Islands pound', currencyCode: 'FKP', currencySymbol: '£', id: "FLK", name: "Falkland Islands (Malvinas)", dailCode: '500'},
  { currency: 'British pound', currencyCode: 'GBP', currencySymbol: '£', id: "GGY", name: "Guernsey", dailCode: '44'},
  { currency: 'British pound', currencyCode: 'GBP', currencySymbol: '£', id: "IMN", name: "Isle of Man", dailCode: '44'},
  { currency: 'Indian rupee', currencyCode: 'INR', currencySymbol: '', id: "IND", name: "India", dailCode: '91'},
  { currency: 'Lebanese pound', currencyCode: 'LBP', currencySymbol: '£', id: "LBN", name: "Lebanon", dailCode: '961'},
  { currency: 'New Zealand dollar', currencyCode: 'NZD', currencySymbol: '$', id: "NZL", name: "New Zealand", dailCode: '64'},
  { currency: 'United States dollar', currencyCode: 'USD', currencySymbol: '$', id: "TLS", name: "Timor-Leste", dailCode: '670'},
  { currency: 'Singapore dollar', currencyCode: 'SGD', currencySymbol: '$', id: "SGP", name: "Singapore", dailCode: '65'},
  { currency: 'United Arab Emirates dirham', currencyCode: 'AED', currencySymbol: 'إ.د', id: 'ARE', name: 'United Arab Emirates', dailCode: '971'},
  { currency: 'British pound', currencyCode: 'GBP', currencySymbol: '£', id: 'GBR', name: 'United Kingdom', dailCode: '44'},
  { currency: 'United States dollar', currencyCode: 'USD', currencySymbol: '$', id: 'USA', name: 'United States', dailCode: '1'},
];

const [selectedOption, setSelectedOption] = React.useState();

const { filteredOptions, getSearchInputProps, searchTerm } = useFilteredOptions({
  initialOptions: countries,
  searchFunction: ({ options, searchTerm }) => {
    if (searchTerm === "") return options;
    return options.filter((option) => {
      const getOnlySearchableKeys = Object.fromEntries(Object.entries(option).filter(([key, index]) => 
                                      key !== 'currencyCode' && key !== 'currency')
                                    );
      return Object.values(getOnlySearchableKeys).map((value) => 
        value.toLowerCase()
      ).join(" ").includes(searchTerm.toLowerCase());
    })
  },
});

React.useLayoutEffect(() => {
  const usersCountry = countries.find((country) => country.dailCode === '91');
  if (usersCountry) {
    setSelectedOption(usersCountry);
  }
}, []);

function NoResults() {
  return (
    <Text className="text-secondary text-center text-body-12 py-4">No matching results</Text>
  );
}

function escapeRegExp(string) {
	return string.replace(/[+$]/g, '\\$&');
}

function highlightMatchedContent(text, searchTerm) {
	if (!searchTerm) return text;
	const escapedSearchTerm = escapeRegExp(searchTerm);
	const regex = new RegExp(`(${escapedSearchTerm})`, 'gi');
	const parts = text.trim().split(regex);
	return parts.map((part, index) =>
		regex.test(part) ? (
			<span className="font-stronger" key={index}>
				{part}
			</span>
		) : (
			part
		)
	);
}

return (
  <Field label="Select Country Code" classNames={{ label: 'whitespace-nowrap'}} className="w-20">
    <FilterMenu popoverMaxWidth={280} popoverMaxHeight={300}>
      <FilterMenuTrigger placeholder="Select Country"
        {...(selectedOption
					? {
							children: `+${selectedOption.dailCode}`,
					  }
					: null)
        }
      />
      <FilterMenuPopover>
        <FilterMenuSearchField label="Search Items">
          <FilterMenuSearchInput
            {...getSearchInputProps()}
            placeholder="Search..."
          />
        </FilterMenuSearchField>
        <FilterMenuListbox
          noResultsFallback={<NoResults />}
          options={filteredOptions}
        >
          {(option) => (
            <FilterMenuItem
              id={option.id}
              isSelected={selectedOption.id === option.id}
              onClick={() => {
                setSelectedOption(option);
              }}
            >
              <Track
                className="gap-1"
                classNames={{
                  center: 'grow-0 line-clamp-1',
                }}
                railEnd={<>+{highlightMatchedContent(option.dailCode, searchTerm)}</>}
              >
                {highlightMatchedContent(option.name, searchTerm)}
              </Track>
            </FilterMenuItem>
          )}
        </FilterMenuListbox>
      </FilterMenuPopover>
    </FilterMenu>
  </Field>
);
  • To get all the currency and country data needed to build this component, check out Business utilities package.
  • The popoverMaxWidth, popoverMaxHeight, popoverPlacement, and popoverMatchReferenceWidth props have the same effect on the FilterMenu component as the Dropdown Menu.
  • To control the size of the Trigger, use the width class on the <Field> component.
  • To control the Trigger appearance to "subtle," wrap <FilterMenu> with Inline Field.

Strategy

Use the strategy prop to change the way how popover element will be positioned. By default, the strategy is set to absolute, changes it to fixed when the FilterMenuTrigger is inside a sticky or fixed element.

This option leverages the floating-ui library, which powers the FilterMenuPopover functionality.

const initialOptions = [
  { code: "61", id: "AUS", name: "Australia" },
  { code: "64", id: "NZL", name: "New Zealand" },
  { code: "91", id: "IND", name: "India" },
  { code: "48", id: "POL", name: "Poland" },
  { code: "44", id: "GBR", name: "Scotland (UK)" },
  { code: "1", id: "USA", name: "United States" },
];

const [selectedOption, setSelectedOption] = React.useState(
  initialOptions[0]
);

const [showFixedElement, setShowFixedElement] = React.useState(false);

const onButtonClick = () => {
	setShowFixedElement((prevState) => !prevState)
}

const [searchTerm, setSearchTerm] = React.useState('');

function onClear(){
  return setSearchTerm('');
}

function handleInputOnChange(event) {
  return setSearchTerm(event.target.value);
}

const filteredOptions = searchTerm === "" ? initialOptions : initialOptions.filter((country) =>
    country.name.toLowerCase().includes(searchTerm.toLowerCase())
);

function OptionLabel({ name, code }) {
  return (
    <Track as="span">
      {name} +{code}
    </Track>
  );
}

function NoResults() {
  return (
    <Text className="text-secondary text-center text-body-12 py-4">
      No matching results
    </Text>
  );
}

return (
	<Stack className="w-full gap-4">
		{showFixedElement ? (
			<div 
        className="
          animate-[snackbar-transition_0.3s_cubic-bezier(0.16,_1,_0.3,_1)]
          bg-neutral-secondary
          fixed
          flex
          items-center
          justify-between
          mx-2
          p-4
          right-0
          rounded-8px
          shadow-40
          sm:right-8
          sm:w-[360px]
          top-8
          w-[calc(100%-16px)]
          z-10
        "
      >
        <Field label="Select Items">
          <FilterMenu strategy="fixed">
            <FilterMenuTrigger placeholder="Select Country">
              {selectedOption ? <OptionLabel {...selectedOption} /> : null}
            </FilterMenuTrigger>
            <FilterMenuPopover>
              <FilterMenuSearchField label="Search Items">
                <FilterMenuSearchInput
                  onClear={onClear}
                  onChange={handleInputOnChange}
                  value={searchTerm}
                  placeholder="Search..."
                />
              </FilterMenuSearchField>
              <FilterMenuListbox
                noResultsFallback={<NoResults />}
                options={filteredOptions}
              >
                {(option) => (
                  <FilterMenuItem
                    id={option.id}
                    isSelected={selectedOption.id === option.id}
                    onClick={() => {
                      setSelectedOption(option);
                    }}
                  >
                    <OptionLabel {...option} />
                  </FilterMenuItem>
                )}
              </FilterMenuListbox>
            </FilterMenuPopover>
          </FilterMenu>
        </Field>
        <button
          className="
            focus-visible:focus-ring
            font-stronger
            px-1
            py-0.5
            rounded-4px
            text-body-12
            text-primary
            underline
            underline-offset-2
          "
          onClick={onButtonClick}
        >
          Close
        </button>
			</div>
		) : null}
		<Button onClick={onButtonClick}>
			Show fixed element
		</Button>
	</Stack>
);

Disabled

Utilize the isDisabled prop in the <Field/> to show that a entire FilterMenu isn't usable.

, Australia +61
const initialOptions = [
  { code: "61", id: "AUS", name: "Australia" },
  { code: "64", id: "NZL", name: "New Zealand" },
  { code: "91", id: "IND", name: "India" },
  { code: "48", id: "POL", name: "Poland" },
  { code: "44", id: "GBR", name: "Scotland (UK)" },
  { code: "1", id: "USA", name: "United States" },
];

const [selectedOption, setSelectedOption] = React.useState(
  initialOptions[0]
);

const [searchTerm, setSearchTerm] = React.useState('');

function onClear(){
  return setSearchTerm('');
}

function handleInputOnChange(event) {
  return setSearchTerm(event.target.value);
}

const filteredOptions = searchTerm === "" ? initialOptions : initialOptions.filter((country) =>
    country.name.toLowerCase().includes(searchTerm.toLowerCase())
);

function OptionLabel({ name, code }) {
  return (
    <Track as="span">
      {name} +{code}
    </Track>
  );
}

function NoResults() {
  return (
    <Text className="text-secondary text-center text-body-12 py-4">
      No matching results
    </Text>
  );
}

return (
  <Field label="Select Items" isDisabled>
    <FilterMenu>
      <FilterMenuTrigger placeholder="Select country">
        {selectedOption ? <OptionLabel {...selectedOption} /> : null}
      </FilterMenuTrigger>
      <FilterMenuPopover>
        <FilterMenuSearchField label="Search Items">
          <FilterMenuSearchInput
            onClear={onClear}
            onChange={handleInputOnChange}
            value={searchTerm}
            placeholder="Search..."
          />
        </FilterMenuSearchField>
        <FilterMenuListbox
          noResultsFallback={<NoResults />}
          options={filteredOptions}
        >
          {(option) => (
            <FilterMenuItem
              id={option.id}
              isSelected={selectedOption.id === option.id}
              onClick={() => {
                setSelectedOption(option);
              }}
            >
              <OptionLabel {...option} />
            </FilterMenuItem>
          )}
        </FilterMenuListbox>
      </FilterMenuPopover>
    </FilterMenu>
  </Field>
);

Disabled MenuItem

Utilize the isDisabled prop in the <FilterMenuItem /> component to indicate that a specific item is not selectable.

This enhances user experience by clearly displaying the disabled state. Additionally, for improved accessibility, keyboard navigation will bypass disabled MenuItems in the Listbox.

, Australia +61
const initialOptions = [
  { code: "61", id: "AUS", name: "Australia", disabled: true},
  { code: "64", id: "NZL", name: "New Zealand", disabled: false},
  { code: "91", id: "IND", name: "India", disabled: false},
  { code: "48", id: "POL", name: "Poland", disabled: true},
  { code: "44", id: "GBR", name: "Scotland (UK)", disabled: false},
  { code: "1", id: "USA", name: "United States", disabled: true},
];

const [selectedOption, setSelectedOption] = React.useState(
  initialOptions[0]
);

const [searchTerm, setSearchTerm] = React.useState('');

function onClear(){
  return setSearchTerm('');
}

function handleInputOnChange(event) {
  return setSearchTerm(event.target.value);
}

const filteredOptions = searchTerm === "" ? initialOptions : initialOptions.filter((country) =>
    country.name.toLowerCase().includes(searchTerm.toLowerCase())
);

function OptionLabel({ name, code }) {
  return (
    <Track as="span">
      {name} +{code}
    </Track>
  );
}

function NoResults() {
  return (
    <Text className="text-secondary text-center text-body-12 py-4">
      No matching results
    </Text>
  );
}

return (
  <Field label="Select Items">
    <FilterMenu>
      <FilterMenuTrigger>
        <OptionLabel {...selectedOption} />
      </FilterMenuTrigger>
      <FilterMenuPopover>
        <FilterMenuSearchField label="Search Items">
          <FilterMenuSearchInput
            onClear={onClear}
            onChange={handleInputOnChange}
            value={searchTerm}
            placeholder="Search..."
          />
        </FilterMenuSearchField>
        <FilterMenuListbox
          noResultsFallback={<NoResults />}
          options={filteredOptions}
        >
          {(option) => (
            <FilterMenuItem
              id={option.id}
              isDisabled={option.disabled}
              isSelected={selectedOption.id === option.id}
              onClick={() => {
                setSelectedOption(option);
              }}
            >
              <OptionLabel {...option} />
            </FilterMenuItem>
          )}
        </FilterMenuListbox>
      </FilterMenuPopover>
    </FilterMenu>
  </Field>
);

Note: Disabled item can already be selected option but having any interactions on it won't be possible.

FullScreen on Mobile

On mobile, enable fullScreenForMobile to render the sheet in full-screen with a default close button.\

, Item 1
const initialOptions = [
  { id: "1", value: "Item 1" },
  { id: "2", value: "Item 2" },
  { id: "3", value: "Item 3" },
];
const [selectedOption, setSelectedOption] = React.useState(initialOptions[0]);

const { filteredOptions, getSearchInputProps } = useFilteredOptions({
  initialOptions,
  searchFunction: ({ options, searchTerm }) => {
    if (searchTerm === "") return options;
    return options.filter((option) =>
      option.value.toLowerCase().includes(searchTerm.toLowerCase())
    );
  },
});

return (
  <Field label="Select Items">
    <FilterMenu fullScreenForMobile>
      <FilterMenuTrigger placeholder="Select">
        {selectedOption.value}
      </FilterMenuTrigger>
      <FilterMenuPopover>
        <FilterMenuSearchField label="Search Items">
          <FilterMenuSearchInput
            {...getSearchInputProps()}
            placeholder="Search..."
          />
        </FilterMenuSearchField>
        <FilterMenuListbox
          noResultsFallback={
            <Text className="text-secondary text-center text-body-12 py-4">
              No matching results
            </Text>
          }
          options={filteredOptions}
        >
          {(option) => (
            <FilterMenuItem
              id={option.id}
              isSelected={selectedOption.id === option.id}
              onClick={() => {
                setSelectedOption(option);
              }}
            >
              {option.value}
            </FilterMenuItem>
          )}
        </FilterMenuListbox>
      </FilterMenuPopover>
    </FilterMenu>
  </Field>
);

API Reference

FilterMenu

PropDefaultDescription
children_((menuState: { isMenuOpen: boolean; triggerProps: TriggerProps }) => ReactNode) | ReactNode
Accepts either a React node or a render function. The render function provides the menu's state (isMenuOpen) and accessibility/interaction props (triggerProps) for the trigger.
closeButtonPropsForMobile?_{ label: string, onClick: () => void, size?: IconButtonProps['size'] }
Props for the close button that appears on mobile.
mobileFriendly?trueboolean
Indicates whether the filter menu should be displayed as a sheet on mobile devices.
popoverMatchReferenceWidth?falseboolean
Match the width of the popover with the reference element.
popoverMaxHeight?356number
The max height of the filter menu popover.
popoverMaxWidth?400number
The max width of the filter menu popover.
popoverOffset?4number
The offset of the filter menu popover.
popoverPlacement?'bottom-start''bottom' | 'bottom-start' | 'bottom-end'
The placement of the filter menu popover in relation to the trigger.
strategy?'absolute''absolute' | 'fixed'
The strategy used to position the floating element.
titleForMobile?_string
If titleForMobile is provided then the mobile sheet view will have a header with title rendered
fullScreenForMobilefalseboolean
Enables fullscreen mode for the mobile select menu, making it cover the entire viewport.

FilterMenuTrigger

The FilterMenuTrigger component is a customizable trigger for a filter menu, built using the Button component. It inherits some Button props and adds few more additional functionalities.

PropDefaultDescription
children?_ReactNode
The content to be displayed inside the button.
form?_string
Specifies the form element the button is associated with.

⚠️ Deprecated, FilterMenuTrigger doesn't need form and it will be removed in future releases.

iconEnd?_ReactNode
The icon to display after the button children.
iconStart?_ReactNode
The icon to display before the button children.

⚠️ Deprecated, FilterMenuTrigger uses a standard chevron icon pattern and doesn't need custom start icons.

isLoading?falseboolean
If true, the button will display a loading indicator.

⚠️ Deprecated, FilterMenuTrigger is a simple toggle control that doesn't perform async operations requiring loading states.

isPressed?falseboolean
If true, the button will be visually styled as pressed and the aria-pressed attribute will be set accordingly.

⚠️ Deprecated, FilterMenuTrigger doesn't need isPressed state and it will be removed in future releases.

loadingLabel?_string
Text to read out to assistive technologies when button is loading.

⚠️ Deprecated, FilterMenuTrigger doesn't display loading states, so loading labels are unnecessary.

onBlur?_function
Function to call when the button loses focus.
onFocus?_function
Function to invoke when the button receives focus.
onKeyDown?_function
Function to invoke when a key is pressed while the button is focused.
placeholder?_string
Displays a placeholder text when no children is provided.

FilterMenuSearchField

The FilterMenuSearchField internally uses Field component. It inherits most of the Field props and adds few more functionalities.

PropDefaultDescription
children_ReactNode
The content to be displayed inside the field.
label_ReactNode
Label for the input.
controlId?_string
Specifies the unique identifier for the form control within the component. See Field API for more details.
counter?_string
Counter object containing maxValue and value. "maxValue:" maximum value the counter can reach. "value:" current value of the counter.

⚠️ Deprecated, FilterMenuSearchField search inputs don't need character counters as they're typically used for textarea.

description?_ReactNode
Provide additional information that will aid user input.

⚠️ Deprecated, FilterMenuSearchField is a simplified search field that doesn't need description.

errorMessage?_ReactNode
Message to show when the field is invalid.

⚠️ Deprecated, FilterMenuSearchField doesn't require validation, so error messages are unnecessary.

isDisabled?falseboolean
Whether the field is disabled.

⚠️ Deprecated, FilterMenuSearchField doesn't need disabling the search input in FilterMenu component.

isRequired?falseboolean
Whether user input is required on the field.

⚠️ Deprecated, FilterMenuSearchField doesn't require validation, so isRequired is unnecessary.

labelVisibility?'hidden''visible' | 'hidden'
A label must always be provided for assistive technology, but you may hide it from sighted users when the intent can be inferred from context.

⚠️ Deprecated, FilterMenuSearchField internally manages label visibility and sets it to hidden by default.

secondaryLabel?_ReactNode
Additional context, typically used to indicate that the field is optional.

⚠️ Deprecated, FilterMenuSearchField is a simplified search field that doesn't need secondary labels.

FilterMenuSearchInput

The FilterMenuSearchInput component internally uses SearchInput component. It inherits most of the SearchInput props and adds few more functionalities.

PropDefaultDescription
aria-activedescendant?_string
identifies the currently active element when using ARIA widgets.

⚠️ Deprecated, FilterMenuSearchInput internally sets aria-activedescendant based on device type and selected item.

aria-autocomplete?_'inline' | 'list' | 'both' | 'none'
Indicates whether input completion suggestions are provided, and their type.

⚠️ Deprecated, FilterMenuSearchInput internally sets aria-autocomplete to 'list' for proper accessibility.

aria-controls?_string
Identifies the element(s) that the input controls.

⚠️ Deprecated, FilterMenuSearchInput internally sets aria-controls to the listId from dropdown context.

aria-expanded?_boolean
Indicates whether the popover is currently expanded or collapsed.

⚠️ Deprecated, FilterMenuSearchInput internally sets aria-expanded based on menu open state from context.

disabled?_boolean
Disables the search input.

⚠️ Deprecated, FilterMenuSearchInput doesn't need disabling the search input in FilterMenu component.

aria-haspopup?_'dialog' | 'grid' | 'listbox' | 'menu' | 'tree' | 'boolean'
Indicates the availability of a popup related to the input.
autoComplete?_string
Specifies whether autocomplete is enabled for the input.

⚠️ Deprecated, FilterMenuSearchInput internally sets autoComplete to 'off' to prevent browser suggestions.

autoCorrect?_'on' | 'off'
Specifies whether auto-correction is enabled for the input.

⚠️ Deprecated, FilterMenuSearchInput internally sets autoCorrect to 'off' to prevent text correction.

autoFocus?_boolean
Automatically focuses the input element when it is rendered.
defaultValue?_string
Specifies the initial value of the input for uncontrolled input.
focusContainerRef?_React.Ref<HTMLDivElement>
Reference to the wrapper FocusContainer element.
inputMode?_'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'
Hints to browsers about what kind of virtual keyboard to display.
name?_string
Name of the input, used for form submissions.

⚠️ Deprecated, FilterMenuSearchInput doesn't need form association, so name attribute is unnecessary.

onBlur?_function
Function to be invoked when the input loses focus.
onChange?_function
Function to be invoked when a new item is selected.
onFocus?_function
Function to be invoked when the input is focused.
onKeyDown?_function
Function to be invoked when a key is pressed while the input is focused.
onPaste?_function
Function to be invoked when content is pasted into the input.
pattern?_string
Regular expression pattern the input's value must match for validation.
placeholder?_string
Placeholder text displayed when the input is empty.
role?_string
ARIA role for the input.

⚠️ Deprecated, FilterMenuSearchInput internally sets role to 'combobox' for proper accessibility.

spellCheck?_boolean
Specifies whether the input value should be checked for spelling errors.

⚠️ Deprecated, FilterMenuSearchInput internally sets spellCheck to 'false' to prevent spell checking.

type?_'email' | 'password' | 'search' | 'tel' | 'text' | 'url'
Type of the input element.
value?_string
Specifies the current value for controlled inputs.

FilterMenuPopover

PropDefaultDescription
children_ReactNode
Content of the filter menu popover.
shouldUsePortal?trueboolean
Determines whether the popover should be rendered in a React Portal. If true, the popover will be rendered outside the DOM hierarchy of the parent component.

FilterMenuListbox

PropDefaultDescription
noResultsFallback?_ReactNode
Component to render when there are no options left in the filtered result.
options?_Array of Objects
Options to be rendered in the popover. Note: Each item in the object should have unique id of key property for better caching of children

FilterMenuItem

PropDefaultDescription
children_ReactNode
The content of the filter menu item.
id?_string
An optional ID for the menu item. If not provided, an ID will be automatically generated.
isDisabled?falseboolean
Indicates if the menu item is currently disabled. Used for styling and accessibility. Applies an aria-disabled attribute.
isHighlighted?falseboolean
Indicates if the menu item is currently highlighted. Used for styling. Applies a data-highlighted attribute. Note: Controlled internally to support keyboard navigations.
isSelected?falseboolean
Indicates if the menu item is currently selected. Used for styling. Applies an aria-selected=true and data-selected=true otherwise aria-selected=false
onClick?_() => boolean | void
Function to be invoked when the item is clicked.
railEnd?_ReactNode
The content/components to appear in the item end.

⚠️ Deprecated, FilterMenuItem doesn't need railEnd for FilterMenu.

railStart?_ReactNode
The content/components to appear in the item start.

⚠️ Deprecated, FilterMenuItem uses railStart internally to set CheckIcon for selected items.

showSelectionIndicator?trueboolean
Controls whether to show the selection indicator (check icon) for selected items.
size?'standard''standard' | 'large'
The size of the filter menu item.

⚠️ Deprecated, the size is now automatically determined based on the Field context.

verticalAlign?'middle''bottom' | 'middle' | 'top'
Determines how the rails and center are vertically aligned to each other. A typography or heading can be provided to center align icons and with text that may wrap.

Style API

Our design system components include style props that allow you to easily customize different parts of each component to match your design needs.

Please refer to the Style API documentation for more insights.