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 totrue
to make the popover’s width match the trigger’s width, orfalse
for 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.
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.