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.

FullScreen on Mobile

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

, Select an animal
const animals = [
  { id: "1", value: "Elephant" },
  { id: "2", value: "Lion" },
  { id: "3", value: "Tiger" },
  { id: "4", value: "Zebra" },
];
const [selectedOption, setSelectedOption] = React.useState();

return (
  <Field label="Animals">
    <SelectMenu fullScreenForMobile>
      <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>
);

API Reference

SelectMenu

PropsTypeDescriptionDefault
children((menuState: { isMenuOpen: boolean; triggerProps: TriggerProps }) => ReactNode) | ReactNodeAccepts 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?booleanWhether the select menu should be mobile friendly.true
titleForMobile?stringThe title of the Modal that appears on mobile instead of Popover._
togglePoint?numberThe toggle point for the select menu to switch to mobile view. Deprecated with backwards compatibility ⚠️ (The logic will be handled internally in future)768
popoverMatchReferenceWidth?booleanMatch the width of the popover with the reference element.false
popoverMaxHeight?numberThe max height of the select menu popover.356
popoverMaxWidth?numberThe max width of the select menu popover.400
popoverOffset?numberThe offset of the select menu popover.4
popoverPlacement?'bottom' | 'bottom-start' | 'bottom-end'The placement of the select menu popover in relation to the trigger.'bottom-start'
selectedOption?objectThe currently selected option. This prop is deprecated and will be removed in future versions. A much better API will be used similar to FilterMenu._
strategy?'absolute' | 'fixed'The strategy used to position the floating element.'absolute'
fullScreenForMobilebooleanEnables fullscreen mode for the mobile select menu, making it cover the entire viewport.false

SelectMenuTrigger

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

PropTypeDescriptionDefault
children?ReactNodeThe content to be displayed inside the button._
form?stringSpecifies the form element the button is associated with._
iconEnd?ReactNodeThe icon to display after the button children._
iconStart?ReactNodeThe icon to display before the button children._
isLoading?booleanIf true, the button will display a loading indicator.false
isPressed?booleanIf true, the button will be visually styled as pressed and the aria-pressed attribute will be set accordingly.false
loadingLabel?stringText to read out to assistive technologies when button is loading._
onBlur?functionFunction to call when the button loses focus._
onFocus?functionFunction to invoke when the button receives focus._
onKeyDown?functionFunction to invoke when a key is pressed while the button is focused._
placeholder?stringDisplays a placeholder text when selectedOption is not provided._

SelectMenuPopover

PropTypeDescriptionDefault
childrenReactNodeContent of the select menu popover._
shouldUsePortal?booleanDetermines 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.true

SelectMenuListbox

PropTypeDescriptionDefault
options?Array of ObjectsOptions to be rendered in the popover. Note: Each item in the object should have unique id of key property for better caching of children_

SelectMenuItem

PropsTypeDescriptionDefault
childrenReactNodeThe content of the select menu item._
id?stringAn optional ID for the menu item. If not provided, an ID will be automatically generated._
isDisabled?booleanIndicates if the menu item is currently disabled. Used for styling and accessibility. Applies an aria-disabled attribute.false
isHighlighted?booleanIndicates if the menu item is currently highlighted. Used for styling. Applies a data-highlighted attribute. Note: Controlled internally to support keyboard navigations.false
isSelected?booleanIndicates if the menu item is currently selected. Used for styling. Applies an aria-selected=true and data-selected=true otherwise aria-selected=falsefalse
onClick?() => boolean | voidFunction to be invoked when the item is clicked._
showSelectionIndicator?booleanControls whether to show the selection indicator (check icon) for selected items.true
railEnd?ReactNodeThe content/components to appear in the item end._
railStart?ReactNodeThe content/components to appear in the item start._
size?'standard' | 'large'The size of the dropdown menu item.'standard'
verticalAlign?'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.'middle'

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.

Mobile Sheet Style API parts

, Select an animal
const animals = [
  { id: "1", value: "Elephant" },
  { id: "2", value: "Lion" },
  { id: "3", value: "Tiger" },
  { id: "4", value: "Zebra" },
];
const [selectedOption, setSelectedOption] = React.useState();

return (
  <Field label="Animals">
    <SelectMenu>
      <SelectMenuTrigger placeholder="Select an animal">
        {selectedOption?.value}
      </SelectMenuTrigger>
      <SelectMenuPopover
        classNames={{
          sheet: "bg-positive",
          sheetWrapper: "bg-caution",
          sheetHeader: "bg-accent-inverse",
          sheetContent: "bg-critical",
        }}
      >
        <SelectMenuListbox options={animals}>
          {(animal) => {
            return (
              <SelectMenuItem
                id={animal.id}
                isSelected={selectedOption?.id === animal.id}
                onClick={() => {
                  setSelectedOption(animal);
                }}
              >
                {animal.value}
              </SelectMenuItem>
            );
          }}
        </SelectMenuListbox>
      </SelectMenuPopover>
    </SelectMenu>
  </Field>
);

Stylable PartsDescription
sheetThe Inner component wrapping the header, content, footer
sheetWrapperThe overlay component.
sheetHeaderThe Header component in Sheet.
sheetContentThe children components in Sheet.
sheetFooterThe footer component in Sheet.
closeButtonThe closeButton in Sheet.