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;
Dropdown menu
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 layoutDropdownMenu.ListandScrollArea—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;
Menu item types
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;
Menu item with actions
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;
Last updated: