Skip to content

DropdownMenu

TIP

If you need to customize your work of the dropdown menu, refer to the documentation for intergalactic/popper

The component is a wrapper over the intergalactic/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 Button from '@semcore/ui/button';
import DropdownMenu from '@semcore/ui/dropdown-menu';
import FileExportM from '@semcore/ui/icon/FileExport/m';
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) var(--intergalactic-spacing-2x)',
              borderWidth: 0,
              borderTopWidth: '1px',
              borderRadius:
                '0 0 var(--intergalactic-rounded-medium) var(--intergalactic-rounded-medium)',
            }}
          >
            <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 Button from '@semcore/ui/button';
import DropdownMenu from '@semcore/ui/dropdown-menu';
import DesktopIconM from '@semcore/ui/icon/Desktop/m';
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 { Flex } from '@semcore/ui/base-components';
import Button from '@semcore/ui/button';
import DropdownMenu from '@semcore/ui/dropdown-menu';
import ChevronRightIcon from '@semcore/ui/icon/ChevronRight/m';
import PlusM from '@semcore/ui/icon/MathPlus/m';
import TrashM from '@semcore/ui/icon/Trash/m';
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 Button from '@semcore/ui/button';
import DropdownMenu from '@semcore/ui/dropdown-menu';
import ChevronRightIcon from '@semcore/ui/icon/ChevronRight/m';
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 { 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 ChevronRightIcon from '@semcore/ui/icon/ChevronRight/m';
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 { Flex } from '@semcore/ui/base-components';
import Button from '@semcore/ui/button';
import DropdownMenu from '@semcore/ui/dropdown-menu';
import Trash from '@semcore/ui/icon/Trash/m';
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 { 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 { Flex, Box, ScreenReaderOnly } from '@semcore/ui/flex-box';
import EditM from '@semcore/ui/icon/Edit/m';
import PlusM from '@semcore/ui/icon/MathPlus/m';
import Settings from '@semcore/ui/icon/Settings/m';
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 `Project ${index}`;
    }),
  };
});

const listHeight = 200;

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

  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 handleKeydownCreateButton = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' || e.key === ' ') {
      setVisible(false);

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

  const filteredProjects = groups.reduce<string[]>((acc, { projects }) => {
    projects.forEach((project) => {
      if (project.toLowerCase().includes(searchValue.toLowerCase())) {
        acc.push(project);
      }
    });

    return acc;
  }, []);

  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} aria-describedby={searchValue ? 'search-result' : undefined} />

        <DropdownMenu.List hMax={listHeight + 41} topOffset={36} shadowSize={5} shadowTheme={{ horizontalTop: 'dark', horizontalBottom: 'light' }}>
          {groups.map((group, index) => {
            if (group.projects.some((project) => {
              return project.toLowerCase().includes(searchValue.toLowerCase());
            }))
              return (
                <DropdownMenu.Group key={index} title={group.title} sticky>
                  {group.projects
                    .filter((project) => project.toLowerCase().includes(searchValue.toLowerCase()))
                    .map((project, index) => (<Row key={`${group.title}_${project}`} data={{ project, setProject, selectedProject }} />))}
                </DropdownMenu.Group>
              );
          })}

          {filteredProjects.length
            ? (
                <ScreenReaderOnly id='search-result' aria-hidden='true'>
                  {filteredProjects.length}
                  {' '}
                  result
                  {filteredProjects.length > 1 && 's'}
                  {' '}
                  found
                </ScreenReaderOnly>
              )
            : (
                <Text
                  tag='div'
                  id='search-result'
                  key='Nothing'
                  p='6px 8px'
                  size={200}
                  use='secondary'
                >
                  Nothing found
                </Text>
              )}
        </DropdownMenu.List>
        <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.