Skip to content

DropdownMenu

TIP

If you need to customize your work of the dropdown menu, refer to the documentation for Popper

The component is a wrapper over Dropdown that allows for the following:

  • Displaying a list of options in a dropdown
  • Scrolling through the list of options using keyboard

Basic usage ​

tsx
import Button from '@semcore/ui/button';
import DropdownMenu from '@semcore/ui/dropdown-menu';
import React from 'react';

const Demo = () => {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger tag={Button}>Actions</DropdownMenu.Trigger>
      <DropdownMenu.Menu>
        <DropdownMenu.Item>Save</DropdownMenu.Item>
        <DropdownMenu.Item>Rename</DropdownMenu.Item>
        <DropdownMenu.Item>Download</DropdownMenu.Item>
        <DropdownMenu.Item>Delete</DropdownMenu.Item>
      </DropdownMenu.Menu>
    </DropdownMenu>
  );
};

export default Demo;

There are a few ways to display the dropdown menu in this component.

First method ​

The easiest way is to use DropdownMenu.Menu.

This is best when you only need to manage the content within the options list.

DropdownMenu.Menu is a wrapper around DropdownMenu.Popper and DropdownMenu.List, and all props pass through to DropdownMenu.List.

tsx
import Button from '@semcore/ui/button';
import DropdownMenu from '@semcore/ui/dropdown-menu';
import React from 'react';

const Demo = () => {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger tag={Button}>Explore menu items</DropdownMenu.Trigger>
      {/* Adding max-height to the dropdown menu */}
      <DropdownMenu.Menu hMax='180px'>
        <DropdownMenu.Group title='List heading' subTitle='Subtitle'>
          <DropdownMenu.Item>Menu item 1</DropdownMenu.Item>
          <DropdownMenu.Item>Menu item 2</DropdownMenu.Item>
          <DropdownMenu.Item>Menu item 3</DropdownMenu.Item>
          <DropdownMenu.Item>Menu item 4</DropdownMenu.Item>
          <DropdownMenu.Item>Menu item 5</DropdownMenu.Item>
          <DropdownMenu.Item>Menu item 6</DropdownMenu.Item>
          <DropdownMenu.Item>Menu item 7</DropdownMenu.Item>
          <DropdownMenu.Item>Menu item 8</DropdownMenu.Item>
          <DropdownMenu.Item>Menu item 9</DropdownMenu.Item>
        </DropdownMenu.Group>
      </DropdownMenu.Menu>
    </DropdownMenu>
  );
};

export default Demo;

Second method ​

Use a combination of two components:

  • DropdownMenu.Popper—for the dropdown layout
  • DropdownMenu.List and ScrollArea—for the option list styles

This method works well when you need flexible customization of the dropdown menu content.

tsx
import FileExportM from '@semcore/icon/FileExport/m';
import Button from '@semcore/ui/button';
import DropdownMenu from '@semcore/ui/dropdown-menu';
import Link from '@semcore/ui/link';
import Notice from '@semcore/ui/notice';
import SpinContainer from '@semcore/ui/spin-container';
import { Text } from '@semcore/ui/typography';
import React from 'react';

const Demo = () => {
  const [loading, setLoading] = React.useState(false);
  const [visible, setVisible] = React.useState(false);

  const triggerRef = React.useRef<HTMLButtonElement | null>(null);

  const handleClick = () => {
    setLoading(true);
    setTimeout(() => {
      setLoading(false);
      setVisible(false);
      triggerRef.current?.focus();
    }, 1000);
  };

  return (
    <DropdownMenu visible={visible} onVisibleChange={setVisible}>
      <DropdownMenu.Trigger tag={Button} ref={triggerRef}>
        <Button.Addon>
          <FileExportM />
        </Button.Addon>
        <Button.Text>Export</Button.Text>
      </DropdownMenu.Trigger>
      <DropdownMenu.Popper wMax='256px' aria-label='Export options'>
        <SpinContainer loading={loading}>
          <DropdownMenu.List>
            <DropdownMenu.Item onClick={handleClick}>Excel</DropdownMenu.Item>
            <DropdownMenu.Item onClick={handleClick}>CSV</DropdownMenu.Item>
            <DropdownMenu.Item onClick={handleClick}>CSV Semicolon</DropdownMenu.Item>
          </DropdownMenu.List>
          <Notice
            aria-labelledby='export-notice-title'
            theme='warning'
            style={{
              padding: 'var(--intergalactic-spacing-3x, 12px) var(--intergalactic-spacing-2x, 8px)',
              borderWidth: 0,
              borderTopWidth: '1px',
              borderRadius:
                '0 0 var(--intergalactic-rounded-medium, 6px) var(--intergalactic-rounded-medium, 6px)',
            }}
          >
            <Notice.Content>
              <Text tag='strong' mb={1} style={{ display: 'block' }} id='export-notice-title'>
                Export failed
              </Text>
              <Text>
                If the problem persists, please contact us at
                {' '}
                <Link inline href='mailto:feedback@semrush.com'>
                  feedback@semrush.com
                </Link>
              </Text>
            </Notice.Content>
          </Notice>
        </SpinContainer>
      </DropdownMenu.Popper>
    </DropdownMenu>
  );
};

export default Demo;

The component offers several options for laying out list item types:

  • DropdownMenu.Item: A list element that can be selected with the keyboard.
  • DropdownMenu.Item.Content: The content within an item, used when you need to include a hint or submenu.
  • DropdownMenu.Item.Addon: Used to add, for example, icons.
  • DropdownMenu.Item.Text: Used for wrapping text if used with addons.
  • DropdownMenu.Item.Hint: A subheading or message with additional information (can't be selected with the keyboard).
tsx
import DesktopIconM from '@semcore/icon/Desktop/m';
import Button from '@semcore/ui/button';
import DropdownMenu from '@semcore/ui/dropdown-menu';
import Tooltip from '@semcore/ui/tooltip';
import React from 'react';

const TooltipContent = () => {
  const tooltipIndex = React.useContext(DropdownMenu.selectedIndexContext);

  return (
    <div>
      Some tooltip for
      {tooltipIndex + 1}
    </div>
  );
};

const Demo = () => {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger tag={Button}>Explore menu item types</DropdownMenu.Trigger>
      <DropdownMenu.Menu>
        <Tooltip placement='right' timeout={[0, 50]}>
          <DropdownMenu.Group title='Menu title' subTitle='Subtitle'>
            <DropdownMenu.Item tag={Tooltip.Trigger}>Menu item 1</DropdownMenu.Item>
            <DropdownMenu.Item tag={Tooltip.Trigger}>
              <DropdownMenu.Item.Content>Menu item 2</DropdownMenu.Item.Content>
              <DropdownMenu.Item.Hint>Hint for menu item 2</DropdownMenu.Item.Hint>
            </DropdownMenu.Item>
            <DropdownMenu.Item tag={Tooltip.Trigger}>
              <DropdownMenu.Item.Content>
                <DropdownMenu.Item.Addon>
                  <DesktopIconM />
                </DropdownMenu.Item.Addon>
                <DropdownMenu.Item.Text>Menu item 3</DropdownMenu.Item.Text>
              </DropdownMenu.Item.Content>
              <DropdownMenu.Item.Hint>Hint for menu item 3</DropdownMenu.Item.Hint>
            </DropdownMenu.Item>
            <DropdownMenu.Item tag={Tooltip.Trigger}>
              <DropdownMenu.Item.Content>Menu item 4</DropdownMenu.Item.Content>
            </DropdownMenu.Item>
          </DropdownMenu.Group>
          <Tooltip.Popper w={120} aria-hidden={true}>
            <TooltipContent />
          </Tooltip.Popper>
        </Tooltip>
      </DropdownMenu.Menu>
    </DropdownMenu>
  );
};

export default Demo;
tsx
import ChevronRightIcon from '@semcore/icon/ChevronRight/m';
import PlusM from '@semcore/icon/MathPlus/m';
import TrashM from '@semcore/icon/Trash/m';
import { Flex } from '@semcore/ui/base-components';
import Button from '@semcore/ui/button';
import DropdownMenu from '@semcore/ui/dropdown-menu';
import React from 'react';

const Demo = () => {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger tag={Button}>Explore menu items with actions</DropdownMenu.Trigger>
      <DropdownMenu.Menu>
        <DropdownMenu.Item>Menu item 1</DropdownMenu.Item>
        <DropdownMenu.Item>Menu item 2</DropdownMenu.Item>

        <DropdownMenu.Item>
          <DropdownMenu inlineActions placement='right'>
            <Flex justifyContent='space-between'>
              <DropdownMenu.Item.Content tag={DropdownMenu.Trigger}>
                Menu item 3
              </DropdownMenu.Item.Content>
              <DropdownMenu.Actions gap={1}>
                <DropdownMenu.Item tag={Button} addonLeft={PlusM} title='Add new' />
                <DropdownMenu.Item tag={Button} addonLeft={TrashM} title='Delete' />
              </DropdownMenu.Actions>
            </Flex>
          </DropdownMenu>
        </DropdownMenu.Item>
        <DropdownMenu.Item>
          <DropdownMenu
            placement='right-start'
            interaction={DropdownMenu.nestedMenuInteraction}
            timeout={[0, 300]}
            offset={[-11, 12]}
          >
            <DropdownMenu.Item.Content tag={DropdownMenu.Trigger}>
              Menu item 4
              <ChevronRightIcon color='icon-secondary-neutral' />
            </DropdownMenu.Item.Content>
            <DropdownMenu.Menu>
              <DropdownMenu.Item>Add</DropdownMenu.Item>
              <DropdownMenu.Item>Delete</DropdownMenu.Item>
            </DropdownMenu.Menu>
          </DropdownMenu>
        </DropdownMenu.Item>
      </DropdownMenu.Menu>
    </DropdownMenu>
  );
};

export default Demo;

Nested menus ​

tsx
import ChevronRightIcon from '@semcore/icon/ChevronRight/m';
import Button from '@semcore/ui/button';
import DropdownMenu from '@semcore/ui/dropdown-menu';
import React from 'react';

const Demo = () => {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger tag={Button}>Explore nested menus</DropdownMenu.Trigger>
      <DropdownMenu.Menu>
        <DropdownMenu.Item>Item 1</DropdownMenu.Item>
        <DropdownMenu.Item>Item 2</DropdownMenu.Item>
        <DropdownMenu.Item>Item 3</DropdownMenu.Item>
        <DropdownMenu.Item>
          <DropdownMenu
            placement='right-start'
            interaction={DropdownMenu.nestedMenuInteraction}
            timeout={[0, 300]}
            offset={[-11, 12]}
          >
            <DropdownMenu.Item.Content tag={DropdownMenu.Trigger}>
              Item 4
              <DropdownMenu.Item.Addon tag={ChevronRightIcon} color='icon-secondary-neutral' />
            </DropdownMenu.Item.Content>
            <DropdownMenu.Menu w={120}>
              <DropdownMenu.Item>
                <DropdownMenu
                  placement='right-start'
                  interaction={DropdownMenu.nestedMenuInteraction}
                  timeout={[0, 300]}
                  offset={[-11, 12]}
                >
                  <DropdownMenu.Item.Content tag={DropdownMenu.Trigger}>
                    Item 4.1
                    <DropdownMenu.Item.Addon
                      tag={ChevronRightIcon}
                      color='icon-secondary-neutral'
                    />
                  </DropdownMenu.Item.Content>
                  <DropdownMenu.Menu w={120}>
                    <DropdownMenu.Item>Item 4.1.1</DropdownMenu.Item>
                    <DropdownMenu.Item>Item 4.1.2</DropdownMenu.Item>
                    <DropdownMenu.Item>Item 4.1.3</DropdownMenu.Item>
                  </DropdownMenu.Menu>
                </DropdownMenu>
              </DropdownMenu.Item>
              <DropdownMenu.Item>
                <DropdownMenu
                  placement='right-start'
                  interaction={DropdownMenu.nestedMenuInteraction}
                  timeout={[0, 300]}
                  offset={[-11, 12]}
                >
                  <DropdownMenu.Item.Content tag={DropdownMenu.Trigger}>
                    Item 4.2
                    <DropdownMenu.Item.Addon
                      tag={ChevronRightIcon}
                      color='icon-secondary-neutral'
                    />
                  </DropdownMenu.Item.Content>
                  <DropdownMenu.Menu w={120}>
                    <DropdownMenu.Item>
                      <DropdownMenu
                        placement='right-start'
                        interaction={DropdownMenu.nestedMenuInteraction}
                        timeout={[0, 300]}
                        offset={[-11, 12]}
                      >
                        <DropdownMenu.Item.Content tag={DropdownMenu.Trigger}>
                          Item 4.2.1
                          <DropdownMenu.Item.Addon
                            tag={ChevronRightIcon}
                            color='icon-secondary-neutral'
                          />
                        </DropdownMenu.Item.Content>
                        <DropdownMenu.Menu w={120}>
                          <DropdownMenu.Item>Item 4.2.1.1</DropdownMenu.Item>
                          <DropdownMenu.Item>Item 4.2.1.2</DropdownMenu.Item>
                          <DropdownMenu.Item>Item 4.2.1.3</DropdownMenu.Item>
                        </DropdownMenu.Menu>
                      </DropdownMenu>
                    </DropdownMenu.Item>
                    <DropdownMenu.Item>Item 4.2.2</DropdownMenu.Item>
                    <DropdownMenu.Item>Item 4.2.3</DropdownMenu.Item>
                  </DropdownMenu.Menu>
                </DropdownMenu>
              </DropdownMenu.Item>
              <DropdownMenu.Item>Item 4.3</DropdownMenu.Item>
            </DropdownMenu.Menu>
          </DropdownMenu>
        </DropdownMenu.Item>
        <DropdownMenu.Item>Item 5</DropdownMenu.Item>
      </DropdownMenu.Menu>
    </DropdownMenu>
  );
};

export default Demo;

Nested menus with focusable elements ​

tsx
import ChevronRightIcon from '@semcore/icon/ChevronRight/m';
import { Box } from '@semcore/ui/base-components';
import Button from '@semcore/ui/button';
import Divider from '@semcore/ui/divider';
import DropdownMenu from '@semcore/ui/dropdown-menu';
import InputNumber from '@semcore/ui/input-number';
import React from 'react';

const options = ['Item 1', 'Item 2', 'Item 3'];
const min = 1;
const max = 8;

const Demo = () => {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger tag={Button}>Explore nested menus</DropdownMenu.Trigger>
      <DropdownMenu.Menu>
        {options.map((item) => {
          return (
            <DropdownMenu.Item key={item}>
              <DropdownMenu
                placement='right-start'
                interaction={DropdownMenu.nestedMenuInteraction}
                timeout={[0, 300]}
                offset={[-11, 12]}
              >
                <DropdownMenu.Item.Content tag={DropdownMenu.Trigger}>
                  {item}
                  <DropdownMenu.Item.Addon tag={ChevronRightIcon} color='icon-secondary-neutral' />
                </DropdownMenu.Item.Content>
                <DropdownMenu.Popper w={150} aria-label='Submenu with controls'>
                  <DropdownMenu.List>
                    <DropdownMenu.Item>Item 4.1.1</DropdownMenu.Item>
                    <DropdownMenu.Item>Item 4.1.2</DropdownMenu.Item>
                    <DropdownMenu.Item>Item 4.1.3</DropdownMenu.Item>
                  </DropdownMenu.List>
                  <Divider my={1} />
                  <Box p={2}>
                    <InputNumber w='50%' neighborLocation='right'>
                      <InputNumber.Value min={min} max={max} placeholder={min.toString()} />
                      <InputNumber.Controls />
                    </InputNumber>
                    <InputNumber w='50%' neighborLocation='left'>
                      <InputNumber.Value min={min} max={max} placeholder={max.toString()} />
                      <InputNumber.Controls />
                    </InputNumber>
                    <Button w='100%' mt={1} use='primary'>
                      Apply
                    </Button>
                  </Box>
                </DropdownMenu.Popper>
              </DropdownMenu>
            </DropdownMenu.Item>
          );
        })}
      </DropdownMenu.Menu>
    </DropdownMenu>
  );
};

export default Demo;

export const App = () => <Demo />;

Selectable menu items ​

tsx
import Trash from '@semcore/icon/Trash/m';
import { Flex } from '@semcore/ui/base-components';
import Button from '@semcore/ui/button';
import DropdownMenu from '@semcore/ui/dropdown-menu';
import React from 'react';

const menuItems: null[] = new Array(10).fill(null);

const Demo = () => {
  const [selected, setSelected] = React.useState<number>(0);

  return (
    <DropdownMenu selectable>
      <DropdownMenu.Trigger tag={Button}>Explore menu items</DropdownMenu.Trigger>
      <DropdownMenu.Menu hMax='180px'>
        <DropdownMenu.Group title='List heading' subTitle='Subtitle'>
          {menuItems.map((_, index) => (
            <DropdownMenu.Item
              key={index}
              selected={index === selected}
              onClick={() => {
                setSelected(index);
              }}
            >
              <DropdownMenu inlineActions placement='right'>
                <Flex justifyContent='space-between'>
                  <DropdownMenu.Item.Content tag={DropdownMenu.Trigger}>
                    Menu item
                    {' '}
                    {index + 1}
                  </DropdownMenu.Item.Content>
                  <DropdownMenu.Actions>
                    <DropdownMenu.Item
                      tag={Button}
                      addonLeft={Trash}
                      title='Delete item'
                      hintPlacement='right'
                      onClick={(e) => e.stopPropagation()}
                    />
                  </DropdownMenu.Actions>
                </Flex>
              </DropdownMenu>
            </DropdownMenu.Item>
          ))}
        </DropdownMenu.Group>
      </DropdownMenu.Menu>
    </DropdownMenu>
  );
};

export default Demo;

Multiselect menu items ​

tsx
import Button from '@semcore/ui/button';
import DropdownMenu from '@semcore/ui/dropdown-menu';
import React from 'react';

const menuItems: null[] = new Array(10).fill(null);

const Demo = () => {
  const [selected, setSelected] = React.useState<number[]>([0, 1]);

  return (
    <DropdownMenu selectable multiselect>
      <DropdownMenu.Trigger tag={Button}>Explore menu items</DropdownMenu.Trigger>
      <DropdownMenu.Menu hMax='180px'>
        <DropdownMenu.Group title='List heading' subTitle='Subtitle'>
          {menuItems.map((_, index) => (
            <DropdownMenu.Item
              key={index}
              selected={selected.includes(index)}
              onClick={() => {
                if (!selected.includes(index)) {
                  setSelected([...selected, index]);
                } else {
                  setSelected(selected.filter((i) => i !== index));
                }
              }}
            >
              Menu item
              {' '}
              {index + 1}
            </DropdownMenu.Item>
          ))}
        </DropdownMenu.Group>
      </DropdownMenu.Menu>
    </DropdownMenu>
  );
};

export default Demo;

Sticky groups titles ​

tsx
import EditM from '@semcore/icon/Edit/m';
import PlusM from '@semcore/icon/MathPlus/m';
import Settings from '@semcore/icon/Settings/m';
import { Flex } from '@semcore/ui/base-components';
import { ButtonTrigger } from '@semcore/ui/base-trigger';
import Button from '@semcore/ui/button';
import Divider from '@semcore/ui/divider';
import DropdownMenu from '@semcore/ui/dropdown-menu';
import { InputSearch } from '@semcore/ui/select';
import { Text } from '@semcore/ui/typography';
import React from 'react';

let index = 0;

const groups = Array.from({ length: 3 }, (_, i) => {
  return {
    title: `Group title ${i}`,
    projects: Array.from({ length: 6 }, (_, j) => {
      index++;
      return {
        title: `Project ${index}`,
      };
    }),
  };
});

const listHeight = 200;

const Row = React.memo(({ style, data: { project, setProject, selectedProject } }: any) => {
  const projectName = project.title;

  return (
    <div style={style}>
      <DropdownMenu.Item
        key={projectName}
        onClick={() => setProject(projectName)}
        selected={selectedProject === projectName}
      >
        <DropdownMenu inlineActions placement='right'>
          <Flex justifyContent='space-between'>
            <DropdownMenu.Item.Content tag={DropdownMenu.Trigger} h={20}>
              {projectName}
            </DropdownMenu.Item.Content>
            <DropdownMenu.Actions gap={2}>
              <DropdownMenu.Item
                tag={Button}
                addonLeft={EditM}
                title='Edit project name'
                hintPlacement='right'
                onClick={(e) => e.stopPropagation()}
              />
              <DropdownMenu.Item
                tag={Button}
                addonLeft={Settings}
                title='Settings'
                hintPlacement='right'
                onClick={(e) => e.stopPropagation()}
              />
            </DropdownMenu.Actions>
          </Flex>
          <DropdownMenu.Item.Hint h={20}>Description</DropdownMenu.Item.Hint>
        </DropdownMenu>
      </DropdownMenu.Item>
    </div>
  );
});

const Demo = () => {
  const [searchValue, setSearchValue] = React.useState('');
  const [visible, setVisible] = React.useState(false);
  const [highlightedIndex, setHighlightedIndex] = React.useState<number | null>(null);
  const [selectedProject, setProject] = React.useState<string | null>(null);
  const [filteredMenuData, setFilteredMenuData] = React.useState(groups);

  const handleKeydownCreateButton = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' || e.key === ' ') {
      setVisible(false);

      e.stopPropagation();
      e.preventDefault();
    }
  };

  React.useEffect(() => {
    const newFilteredProjects: typeof groups = [];
    let highlightedIndex = -1;
    let index = -1;
    groups.forEach((group, i) => {
      group.projects.forEach((item, j) => {
        if (item.title.toLowerCase().includes(searchValue.toLowerCase())) {
          if (!newFilteredProjects[i]) {
            newFilteredProjects[i] = {
              ...group,
              projects: [],
            };
          }
          newFilteredProjects[i].projects.push(item);
          index++;
          if (item.title === selectedProject || highlightedIndex === -1) {
            highlightedIndex = index;
          }
        }
      });
    });
    setHighlightedIndex(highlightedIndex);
    setFilteredMenuData(newFilteredProjects);
  }, [searchValue]);

  return (
    <DropdownMenu
      selectable
      visible={visible}
      onVisibleChange={setVisible}
      highlightedIndex={highlightedIndex}
      onHighlightedIndexChange={setHighlightedIndex}
    >
      <DropdownMenu.Trigger tag={ButtonTrigger} w={220}>
        {selectedProject ?? 'Select project'}
      </DropdownMenu.Trigger>

      <DropdownMenu.Popper aria-label='Select project popover'>
        <InputSearch value={searchValue} onChange={setSearchValue} m={1} autoFocus={false} />

        {filteredMenuData.length > 0 && (
          <DropdownMenu.List hMax={listHeight + 41} topOffset={36} shadowSize={5} shadowTheme={{ horizontalTop: 'dark', horizontalBottom: 'light' }}>
            {filteredMenuData.map((group, index) => {
              return (
                <DropdownMenu.Group key={index} title={group.title} sticky>
                  {group.projects.map((project) => (<Row key={`${group.title}_${project.title}`} data={{ project, setProject, selectedProject }} />))}
                </DropdownMenu.Group>
              );
            })}
          </DropdownMenu.List>
        )}
        {searchValue !== '' && <DropdownMenu.StatusItem itemsCount={filteredMenuData.length} />}
        <Divider />
        <DropdownMenu.Item
          role='button'
          tabIndex={0}
          tag={Flex}
          alignItems='center'
          aria-checked={undefined}
          onKeyDown={handleKeydownCreateButton}
        >
          <DropdownMenu.Item.Addon tag={PlusM} color='text-link' />
          <DropdownMenu.Item.Content tag={Text} color='text-link'>
            Create new project
          </DropdownMenu.Item.Content>
        </DropdownMenu.Item>
      </DropdownMenu.Popper>
    </DropdownMenu>
  );
};

export default Demo;

Released under the MIT License.

Released under the MIT License.