SelectMenu

The SelectMenu is an accessible and customisable dropdown menu that allows users to select an option from the list. In small screens (tablet and mobile), the SelectMenu will be displayed as a modal. Modal is responsive as well, so as a consequence, the SelectMenu will be displayed as a Sheet on mobile. The title for a modal or a sheet can be provided as a prop or can be inherited from the field label.

Quick Start

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

Size

Customise the size of the SelectMenu by using the size prop for the Field.

The default size is "standard".

, Select an animal
const animals = [
  { id: "1", value: "Elephant", emoji: "🐘" },
  { id: "2", value: "Lion", emoji: "🦁" },
  { id: "3", value: "Tiger", emoji: "🐅" },
  { id: "4", value: "Zebra", emoji: "🦓" },
];
const [selectedOption, setSelectedOption] = React.useState();
return (
  <Field
    label="Animals"
    // large | standard
    size="large"
  >
    <SelectMenu>
      <SelectMenuTrigger placeholder="Select an animal">
        {selectedOption?.value}
      </SelectMenuTrigger>
      <SelectMenuPopover>
        <SelectMenuListbox options={animals}>
          {(animal) => {
            return (
              <SelectMenuItem
                id={animal.id}
                isSelected={selectedOption?.id === animal.id}
                onClick={() => {
                  setSelectedOption(animal);
                }}
              >
                {animal.value}
              </SelectMenuItem>
            );
          }}
        </SelectMenuListbox>
      </SelectMenuPopover>
    </SelectMenu>
  </Field>
);

Popover Customisations

The SelectMenu component provides several props to customise the appearance and behavior of its popover. These props allow for fine-grained control over the popover’s width, height, offset, and placement relative to the trigger element.

  • popoverMatchReferenceWidth: Set to true to make the popover’s width match the trigger’s width, or false for independent width.
  • popoverMaxHeight: Sets the maximum height of the popover in pixels. The default is 356.
  • popoverMaxWidth: Sets the maximum width of the popover in pixels. The default is 400.
  • popoverOffset: Adjusts the space between the popover and the trigger, specified in pixels. The default is 4.
  • popoverPlacement: Determines the position of the popover relative to the trigger. Options include 'bottom', 'bottom-start', and 'bottom-end', allowing for flexible positioning based on the layout and space available. The default is 'bottom-start'. If there isn’t enough space for the popover to appear below the trigger, it will automatically switch to the top position.

Here’s how you can use these props in your SelectMenu component:

, Select an animal
const animals = [
  { id: "1", value: "Elephant", emoji: "🐘" },
  { id: "2", value: "Lion", emoji: "🦁" },
  { id: "3", value: "Tiger", emoji: "🐅" },
  { id: "4", value: "Zebra", emoji: "🦓" },
];

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

return (
  <Field label="Animals">
    <SelectMenu
      popoverMatchReferenceWidth={false} // boolean
      popoverMaxHeight={180} // number
      popoverMaxWidth={180} // number
      popoverOffset={12} // number
      popoverPlacement="bottom-start" // 'bottom' | 'bottom-start' | 'bottom-end'
    >
      <SelectMenuTrigger placeholder="Select an animal">
        {selectedOption?.value}
      </SelectMenuTrigger>
      <SelectMenuPopover>
        <SelectMenuListbox options={animals}>
          {(animal) => (
            <SelectMenuItem
              id={animal.id}
              isSelected={selectedOption?.id === animal.id}
              onClick={() => {
                setSelectedOption(animal)
              }}
            >
              {animal.value}
            </SelectMenuItem>
          )}
        </SelectMenuListbox>
      </SelectMenuPopover>
    </SelectMenu>
  </Field>
);

Custom Trigger

You can create a custom trigger for the SelectMenu by providing a callback function as the children prop. This function provides triggerProps, which is an object containing both the props required for the trigger to function, as well as other attributes required for it to be accessibly labelled. It also provides isMenuOpen, which is a boolean indicating the open state of the menu.

const animals = [
  { id: "1", value: "Elephant", emoji: "🐘" },
  { id: "2", value: "Lion", emoji: "🦁" },
  { id: "3", value: "Tiger", emoji: "🐅" },
  { id: "4", value: "Zebra", emoji: "🦓" },
];

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

return (
  <Field label="Animals">
    <SelectMenu>
      {({ triggerProps, isMenuOpen }) => (
        <React.Fragment>
          <Button
            className="w-fit"
            iconEnd={isMenuOpen ? DropdownUpIcon : DropdownDownIcon}
            variant="neutralSecondary"
            {...triggerProps}
          >
            {selectedOption ? (
              <Emoji>{selectedOption.emoji}</Emoji>
            ) : (
              <Text className="text-body-12">Select an animal</Text>
            )}
          </Button>
          <SelectMenuPopover>
            <SelectMenuListbox options={animals}>
              {(animal) => (
                <SelectMenuItem
                  id={animal.id}
                  isSelected={selectedOption?.id === animal.id}
                  onClick={() => setSelectedOption(animal)}
                >
                  {animal.value}
                </SelectMenuItem>
              )}
            </SelectMenuListbox>
          </SelectMenuPopover>
        </React.Fragment>
      )}
    </SelectMenu>
  </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 SelectMenuTrigger is inside a sticky or fixed element.

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

const animals = [
  { id: "1", value: "Elephant", emoji: "🐘" },
  { id: "2", value: "Lion", emoji: "🦁" },
  { id: "3", value: "Tiger", emoji: "🐅" },
  { id: "4", value: "Zebra", emoji: "🦓" },
];

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

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

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

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="Animals">
          <SelectMenu strategy="fixed">
            {({ triggerProps, isMenuOpen }) => (
              <React.Fragment>
                <Button
                  className="w-fit"
                  iconEnd={isMenuOpen ? DropdownUpIcon : DropdownDownIcon}
                  variant="neutralSecondary"
                  {...triggerProps}
                >
                  {selectedOption ? (
                    <Emoji>{selectedOption.emoji}</Emoji>
                  ) : (
                    <Text className="text-body-12">Select an animal</Text>
                  )}
                </Button>
                <SelectMenuPopover>
                  <SelectMenuListbox options={animals}>
                    {(animal) => (
                      <SelectMenuItem
                        id={animal.id}
                        isSelected={selectedOption?.id === animal.id}
                        onClick={() => setSelectedOption(animal)}
                      >
                        {animal.value}
                      </SelectMenuItem>
                    )}
                  </SelectMenuListbox>
                </SelectMenuPopover>
              </React.Fragment>
            )}
          </SelectMenu>
        </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 SelectMenu isn't usable.

, Select an animal
const animals = [
  { id: "1", value: "Elephant", emoji: "🐘" },
  { id: "2", value: "Lion", emoji: "🦁" },
  { id: "3", value: "Tiger", emoji: "🐅" },
  { id: "4", value: "Zebra", emoji: "🦓" },
];
const [selectedOption, setSelectedOption] = React.useState();
return (
  <Field
    label="Animals"
    isDisabled
  >
    <SelectMenu>
      <SelectMenuTrigger placeholder="Select an animal">
        {selectedOption?.value}
      </SelectMenuTrigger>
      <SelectMenuPopover>
        <SelectMenuListbox options={animals}>
          {(animal) => {
            return (
              <SelectMenuItem
                id={animal.id}
                isSelected={selectedOption?.id === animal.id}
                onClick={() => {
                  setSelectedOption(animal);
                }}
              >
                {animal.value}
              </SelectMenuItem>
            );
          }}
        </SelectMenuListbox>
      </SelectMenuPopover>
    </SelectMenu>
  </Field>
);

Disabled MenuItem

Utilize the isDisabled prop in the <SelectMenuItem /> 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.

, Select an animal
const animals = [
  { id: "1", value: "Elephant", emoji: "🐘", disabled: true },
  { id: "2", value: "Lion", emoji: "🦁", disabled: false },
  { id: "3", value: "Tiger", emoji: "🐅", disabled: true },
  { id: "4", value: "Zebra", emoji: "🦓", disabled: false },
];
const [selectedOption, setSelectedOption] = React.useState();
return (
  <Field
    label="Animals"
  >
    <SelectMenu>
      <SelectMenuTrigger placeholder="Select an animal">
        {selectedOption?.value}
      </SelectMenuTrigger>
      <SelectMenuPopover>
        <SelectMenuListbox options={animals}>
          {(animal) => {
            return (
              <SelectMenuItem
                id={animal.id}
                isDisabled={animal.disabled}
                isSelected={selectedOption?.id === animal.id}
                onClick={() => {
                  setSelectedOption(animal);
                }}
              >
                {animal.value}
              </SelectMenuItem>
            );
          }}
        </SelectMenuListbox>
      </SelectMenuPopover>
    </SelectMenu>
  </Field>
);

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

Responsive layout

Utilize the closeButtonPropsForMobile? prop in the <SelectMenu /> component to make sure that in mobile friendly version of the SelectMenu the Modal has a close button.

You can also use the titleForMobile? prop to set the title of the Modal that appears on mobile instead of the Popover.

, Select an animal
const animals = [
{ id: "1", value: "Elephant", emoji: "🐘" },
{ id: "2", value: "Lion", emoji: "🦁" },
{ id: "3", value: "Tiger", emoji: "🐅"},
{ id: "4", value: "Zebra", emoji: "🦓" },
];
const [selectedOption, setSelectedOption] = React.useState();
return (
<Field label="Animals">
  <SelectMenu titleForMobile="Choose an animal" closeButtonPropsForMobile={{
    label: "Close animal select" }}>
    <SelectMenuTrigger placeholder="Select an animal">
      {selectedOption?.value}
    </SelectMenuTrigger>
    <SelectMenuPopover>
      <SelectMenuListbox options={animals}>
        {(animal) => {
        return (
        <SelectMenuItem id={animal.id} isSelected={selectedOption?.id===animal.id} onClick={()=> {
          setSelectedOption(animal);
          }}
          >
          {animal.value}
        </SelectMenuItem>
        );
        }}
      </SelectMenuListbox>
    </SelectMenuPopover>
  </SelectMenu>
</Field>
);

Note: All SelectMenu components are mobile friendly by default.

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.