DropdownMenu

A component that displays a menu to the user with a list of actions or functions when the menu trigger is pressed.

Quick Start

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

Default

The DropdownMenu and it's subcomponents can be composed to display a list of menu items that can trigger an action via the onClick prop:

<DropdownMenu>
  <DropdownMenuTrigger>Toggle dropdown menu</DropdownMenuTrigger>
  <DropdownMenuPopover>
    <DropdownMenuList>
      <DropdownMenuItem onClick={() => alert("Clicked profile")}>
        Profile
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked messages")}>
        Messages
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked settings")}>
        Account settings
      </DropdownMenuItem>
    </DropdownMenuList>
  </DropdownMenuPopover>
</DropdownMenu>

Size

Customize the size of the DropdownMenu by using the size prop.

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

<DropdownMenu size="large">
  <DropdownMenuTrigger>Toggle dropdown menu</DropdownMenuTrigger>
  <DropdownMenuPopover>
    <DropdownMenuList>
      <DropdownMenuItem onClick={() => alert("Clicked profile")}>
        Profile
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked messages")}>
        Messages
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked settings")}>
        Account settings
      </DropdownMenuItem>
    </DropdownMenuList>
  </DropdownMenuPopover>
</DropdownMenu>

Popover height & width control

You can set the maximum size of the DropdownMenu’s popovers using the popoverMaxHeight and popoverMaxWidth props:

<DropdownMenu popoverMaxHeight={100} popoverMaxWidth={150}>
  <DropdownMenuTrigger>Toggle dropdown menu</DropdownMenuTrigger>
  <DropdownMenuPopover>
    <DropdownMenuList>
      <DropdownMenuItem onClick={() => alert("Clicked profile")}>
        Profile
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked messages")}>
        Messages
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked settings")}>
        Account settings
      </DropdownMenuItem>
    </DropdownMenuList>
  </DropdownMenuPopover>
</DropdownMenu>

Popover placement

You can control the position of the popover relative to the trigger by using the popoverPlacement prop. The default value is bottom-start.

The distance between the trigger and the popover can be controlled using the popoverOffset prop. The default value is 8.

<DropdownMenu
  popoverOffset={16}
  popoverPlacement="bottom" // bottom-start | bottom-end
>
  <DropdownMenuTrigger>Toggle dropdown menu</DropdownMenuTrigger>
  <DropdownMenuPopover>
    <DropdownMenuList>
      <DropdownMenuItem onClick={() => alert("Clicked profile")}>
        Profile
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked messages")}>
        Messages
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked settings")}>
        Account settings
      </DropdownMenuItem>
    </DropdownMenuList>
  </DropdownMenuPopover>
</DropdownMenu>

Custom trigger

You can provide the DropdownMenu with a custom trigger by passing a callback function to its children. This function provides access to both the button props required for the trigger to function correctly and be labeled accessibly, as well as the open state of the menu.

<DropdownMenu>
  {({ triggerProps, isMenuOpen }) => (
    <>
      <IconButton
        className="aria-expanded:bg-neutral-pressed"
        icon={isMenuOpen ? LockOpenIcon : LockClosedIcon}
        label="Toggle dropdown menu"
        variant="neutralSecondary"
        {...triggerProps}
      />
      <DropdownMenuPopover>
        <DropdownMenuList>
          <DropdownMenuItem onClick={() => alert("Profile")}>
            Profile
          </DropdownMenuItem>
          <DropdownMenuItem onClick={() => alert("Messages")}>
            Messages
          </DropdownMenuItem>
          <DropdownMenuItem onClick={() => alert("Account settings")}>
            Account settings
          </DropdownMenuItem>
        </DropdownMenuList>
      </DropdownMenuPopover>
    </>
  )}
</DropdownMenu>

Customizing a Custom Trigger:

Here's an example which uses an Avatar and a wrapper Box as custom trigger for the DropdownMenu. Play around with it.

<DropdownMenu>
	{({ triggerProps, isMenuOpen }) => (
		<>
			<Box
				{...triggerProps}
				className="p-1.5 rounded-full cursor-pointer leading-none hover:bg-neutral-secondary-hover focus-visible:focus-ring active:bg-neutral-secondary-pressed aria-expanded:bg-neutral-secondary-pressed"
				tabIndex={0}
			>
				<Avatar name="Freddy" size="32">
					<AvatarIcon icon={ProfileIcon} />
				</Avatar>
			</Box>
			<DropdownMenuPopover>
				<DropdownMenuList>
					<DropdownMenuItem onClick={() => alert('Profile')}>
						Profile
					</DropdownMenuItem>
					<DropdownMenuItem onClick={() => alert('Messages')}>
						Messages
					</DropdownMenuItem>
					<DropdownMenuItem onClick={() => alert('Account settings')}>
						Account settings
					</DropdownMenuItem>
				</DropdownMenuList>
			</DropdownMenuPopover>
		</>
	)}
</DropdownMenu>

You can see that although the custom trigger provided is a non-interactive element, it works as expected with all the accesibility features and UI states included. Here's a breakdown of the API and tokens involved in making a non-interactive trigger into an accessibie trigger.

  • triggerProps - a prop that includes the neccessary a11y attributes and callbacks.
  • isMenuOpen - a prop that includes the open state of the DropdownMenu.
  • tabindex - an HTML attribute that enables focus for an element. See MDN reference for further details.
  • hover:* - a CSS class utilty from Tailwindcss for styling the element on hover.
  • active:* - a CSS class utilty from Tailwindcss for styling the element when active.
  • focus-visible:focus-ring - a CSS class that adds a focus indicator to the element on keyboard focus.
  • aria-expanded:* - a CSS class utilty from Tailwindcss to style the element when aria-expanded=true

Attention 🚧 : Though we are able to customize the custom trigger for any element. It is recommended to use only focusable element as trigger for the DropdownMenu

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 DropdownMenuTrigger is inside a sticky or fixed element.

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

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
      "
      >
        <DropdownMenu strategy="fixed">
          {({ triggerProps, isMenuOpen }) => (
            <>
              <DropdownMenuTrigger>Toggle dropdown</DropdownMenuTrigger>
              <DropdownMenuPopover>
                <DropdownMenuList>
                  <DropdownMenuItem onClick={() => alert("Profile")}>
                    Profile
                  </DropdownMenuItem>
                  <DropdownMenuItem onClick={() => alert("Messages")}>
                    Messages
                  </DropdownMenuItem>
                  <DropdownMenuItem onClick={() => alert("Account settings")}>
                    Account settings
                  </DropdownMenuItem>
                </DropdownMenuList>
              </DropdownMenuPopover>
            </>
          )}
        </DropdownMenu>
        <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 DropdownMenu isn't usable.

<DropdownMenu isDisabled>
  <DropdownMenuTrigger>Toggle dropdown menu</DropdownMenuTrigger>
  <DropdownMenuPopover>
    <DropdownMenuList>
      <DropdownMenuItem onClick={() => alert("Clicked profile")}>
        Profile
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked messages")}>
        Messages
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked settings")}>
        Account settings
      </DropdownMenuItem>
    </DropdownMenuList>
  </DropdownMenuPopover>
</DropdownMenu>

Disabled MenuItem

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

<DropdownMenu>
  <DropdownMenuTrigger>Toggle dropdown menu</DropdownMenuTrigger>
  <DropdownMenuPopover>
    <DropdownMenuList>
      <DropdownMenuItem onClick={() => alert("Clicked profile")}>
        Profile
      </DropdownMenuItem>
      <DropdownMenuItem isDisabled onClick={() => alert("Clicked messages")}>
        Messages
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked settings")}>
        Account settings
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Clicked personal settings")}>
        Personal settings
      </DropdownMenuItem>
    </DropdownMenuList>
  </DropdownMenuPopover>
</DropdownMenu>

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

FullScreen on Mobile

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

<DropdownMenu mobilePopover={{ isFullScreen: true }}>
  <DropdownMenuTrigger>Toggle dropdown menu</DropdownMenuTrigger>
  <DropdownMenuPopover>
    <DropdownMenuList>
      <DropdownMenuItem onClick={() => alert("Profile")}>
        Profile
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Messages")}>
        Messages
      </DropdownMenuItem>
      <DropdownMenuItem onClick={() => alert("Account settings")}>
        Account settings
      </DropdownMenuItem>
    </DropdownMenuList>
  </DropdownMenuPopover>
</DropdownMenu>

API Reference

DropdownMenu

PropDefaultDescription
isDisabled?falseboolean
Whether the dropdown menu is disabled.
children_((menuState: { isMenuOpen: boolean; triggerProps: TriggerProps<React.ElementType>; }) => React.ReactNode) | React.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.
popoverMatchReferenceWidth?falseboolean
Match the width of the popover with the reference element.
popoverMaxHeight?356number
The max height of the dropdown panel popover.
popoverMaxWidth?400number
The max width of the dropdown panel popover.
popoverOffset?4number
The offset of the dropdown panel popover.
popoverPlacement?'bottom-start''bottom' | 'bottom-start' | 'bottom-end'
The placement of the dropdown panel popover relative to the dropdown menu trigger.
size?'standard''standard' | 'large'
The size of the dropdown menu.
strategy?'absolute''absolute' | 'fixed'
The strategy used to position the floating element.
mobilePopover?_object
Config options for responsive behavior of the dropdown menu.
mobilePopover?.mobileFriendlytrueboolean
Whether the dropdown menu should be mobile friendly
mobilePopover?.titleForMobile_string
Title for the mobile popover. If titleForMobile is omitted, the sheet header is not rendered.
mobilePopover?.closeButtonPropsForMobile_{ label: string, onClick: () => void, size?: 'large' | 'standard' | 'small' }
Config options for the close button that appears on mobile.
mobilePopover?.togglePoint767number
The toggle point for the dropdown menu to switch to mobile view.

⚠️ Deprecated with backwards compatibility. The logic will be handled internally in future.

mobilePopover?.isFullScreenfalseboolean
When true, the sheet in mobile view will take up 100% of the available height

DropdownMenuTrigger

PropDefaultDescription
variant?'neutralSecondary''accentPrimary' | 'accentSecondary' | 'criticalPrimary' | 'criticalTertiary' | 'neutralPrimary' | 'neutralSecondary' | 'neutralSecondaryIntense' | 'neutralTertiary'
Variant of the button.
size?'standard''large' | 'standard' | 'small'
Size of the button.
iconStart?_Icon
The icon to display before the buttons children.
iconEnd?_Icon
The icon to display after the buttons children.
loadingLabel?_string
Text to read out to assistive technologies when button is loading.
isDisabled?falseboolean
If true, the button is disabled.
isLoading?falseboolean
If true ,the button will display a loading indicator.
isPressed?_boolean
If true, the button will be visually styled as pressed and the aria-pressed attribute will be set accordingly.
children?_React.ReactNode
The content to be displayed inside the button.
form?_string
Specifies the form element the button is associated with.
onBlur?_function
Function to call when the button loses focus.
onFocus?_function
Function to call when the button receives focus.
onKeyDown?_function
Function to call when a key is pressed while the button is focused.

DropdownMenuPopover

PropDefaultDescription
shouldUsePortal?trueboolean
Determines whether the DropdownMenuPopover should be rendered in a React Portal. If true, the Popover will be rendered outside the DOM hierarchy of the parent component.
children_React.ReactNode
Content of the DropdownMenu with the options.

DropdownMenuList

PropDefaultDescription
children?_React.ReactNode | ((option: object) => React.ReactNode)
Accepts either a ReactNode or a function that returns a ReactNode for each item in the collection.
options?_ReadonlyArray<object>
An array of items that the collection should render.Note: This is mandatory if children is a function.

DropdownMenuItem

PropDefaultDescription
children_React.ReactNode
The content of the dropdown menu item.
id?_string
An optional ID for the menu item; auto-generated if not provided.
onClick?_onClick?: () => boolean | void;
Function to be called when the item is clicked; if it returns false, the dropdown will remain open.
size?standard'standard' | 'large'
Size of the menu item.

Note: The DropdownMenuItem component can render as different HTML elements (e.g., label, a, div) based on the as prop, providing flexibility for various use cases in dropdown menus.

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.