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".
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 totrueto make the popover’s width match the trigger’s width, orfalsefor independent width.popoverMaxHeight: Sets the maximum height of the popover in pixels. The default is356.popoverMaxWidth: Sets the maximum width of the popover in pixels. The default is400.popoverOffset: Adjusts the space between the popover and the trigger, specified in pixels. The default is4.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:
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.
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.
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.
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.
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
| Props | Type | Description | Default |
|---|---|---|---|
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? | boolean | Whether the select menu should be mobile friendly. | true |
titleForMobile? | string | The title of the Modal that appears on mobile instead of Popover. | _ |
togglePoint? | number | The 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? | boolean | Match the width of the popover with the reference element. | false |
popoverMaxHeight? | number | The max height of the select menu popover. | 356 |
popoverMaxWidth? | number | The max width of the select menu popover. | 400 |
popoverOffset? | number | The 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? | object | The 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' |
fullScreenForMobile | boolean | Enables 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.
| Prop | Type | Description | Default |
|---|---|---|---|
children? | ReactNode | The content to be displayed inside the button. | _ |
form? | string | Specifies the form element the button is associated with. | _ |
iconEnd? | ReactNode | The icon to display after the button children. | _ |
iconStart? | ReactNode | The icon to display before the button children. | _ |
isLoading? | boolean | If true, the button will display a loading indicator. | false |
isPressed? | boolean | If true, the button will be visually styled as pressed and the aria-pressed attribute will be set accordingly. | false |
loadingLabel? | string | Text to read out to assistive technologies when button is loading. | _ |
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 selectedOption is not provided. | _ |
SelectMenuPopover
| Prop | Type | Description | Default |
|---|---|---|---|
children | ReactNode | Content of the select menu popover. | _ |
shouldUsePortal? | boolean | 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. | true |
SelectMenuListbox
| Prop | Type | Description | Default |
|---|---|---|---|
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 | _ |
SelectMenuItem
| Props | Type | Description | Default |
|---|---|---|---|
children | ReactNode | The content of the select menu item. | _ |
id? | string | An optional ID for the menu item. If not provided, an ID will be automatically generated. | _ |
isDisabled? | boolean | Indicates if the menu item is currently disabled. Used for styling and accessibility. Applies an aria-disabled attribute. | false |
isHighlighted? | boolean | Indicates if the menu item is currently highlighted. Used for styling. Applies a data-highlighted attribute. Note: Controlled internally to support keyboard navigations. | false |
isSelected? | boolean | Indicates if the menu item is currently selected. Used for styling. Applies an aria-selected=true and data-selected=true otherwise aria-selected=false | false |
onClick? | () => boolean | void | Function to be invoked when the item is clicked. | _ |
showSelectionIndicator? | boolean | Controls whether to show the selection indicator (check icon) for selected items. | true |
railEnd? | ReactNode | The content/components to appear in the item end. | _ |
railStart? | ReactNode | The 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
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 Parts | Description |
|---|---|
sheet | The Inner component wrapping the header, content, footer |
sheetWrapper | The overlay component. |
sheetHeader | The Header component in Sheet. |
sheetContent | The children components in Sheet. |
sheetFooter | The footer component in Sheet. |
closeButton | The closeButton in Sheet. |