Dropdown
A Dropdown is an interactive overlay menu anchored to a trigger element. It is built on top of the Popover component and is designed for action menus, navigation lists, and nested submenus.
Key Characteristics
- Composable: Build menus declaratively using
Dropdown.Menu,Dropdown.Section,Dropdown.Item,Dropdown.Divider,Dropdown.Header, andDropdown.Footer. - Nestable: Create nested submenus with
isNested- opens on hover with a caret indicator. - Polymorphic Items: Render items as any element (
div,a,Link, etc.) using theasprop. - Accessible: Full keyboard navigation, focus management, and proper ARIA attributes built in.
Import
There are 7 dropdown-related components:
Dropdown: The main dropdown component.DropdownTrigger: The element that toggles the dropdown.DropdownMenu: The container for dropdown content.DropdownItem: A clickable menu item.DropdownSection: A group of items with an optional title.DropdownDivider: A visual separator between sections.DropdownHeader/DropdownFooter: Non-clickable top/bottom slots.
import {
Dropdown,
DropdownTrigger,
DropdownMenu,
DropdownItem,
DropdownSection,
DropdownDivider,
} from '@andrejground/lab';
Compound pattern is also supported:
Dropdown.TriggerDropdown.MenuDropdown.ItemDropdown.SectionDropdown.DividerDropdown.HeaderDropdown.Footer
Customization
Add your styles and your component is ready to be used.
- MyDropdown.tsx
- MyDropdown.module.scss
import { Dropdown, DropdownProps } from '@andrejground/lab';
import styles from './MyDropdown.module.scss';
type Props = DropdownProps;
function MyDropdown(props: Props) {
return (
<Dropdown
{...props}
classNames={{
popover: { content: styles.popoverContent },
item: { base: styles.itemBase },
section: { base: styles.sectionBase, title: styles.sectionTitle },
divider: { base: styles.dividerBase },
}}
/>
);
}
MyDropdown.Menu = Dropdown.Menu;
MyDropdown.Header = Dropdown.Header;
MyDropdown.Footer = Dropdown.Footer;
MyDropdown.Section = Dropdown.Section;
MyDropdown.Item = Dropdown.Item;
MyDropdown.Trigger = Dropdown.Trigger;
MyDropdown.Divider = Dropdown.Divider;
export default MyDropdown;
.popoverContent {
background-color: var(--background-color);
color: var(--text-color);
border: 1px solid var(--border-color);
}
.sectionTitle {
background-color: transparent;
font-size: 80%;
}
.itemBase {
&:hover,
&:active,
&:focus-visible,
&:focus-within,
&[data-highlighted-item='true'] {
background-color: var(--item-hover-bg-color);
}
}
.dividerBase {
background-color: var(--divider-color);
}
Usage
- Preview
- Code
import { Dropdown } from '@andrejground/lab';
export default function App() {
return (
<Dropdown>
<Dropdown.Trigger>
<button>Open Menu</button>
</Dropdown.Trigger>
<Dropdown.Menu>
<Dropdown.Item onClick={() => alert('New file')}>
New file
</Dropdown.Item>
<Dropdown.Item onClick={() => alert('Copy link')}>
Copy link
</Dropdown.Item>
<Dropdown.Item onClick={() => alert('Edit')}>
Edit
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
);
}
Sections
Use Dropdown.Section to group items with titles, and Dropdown.Divider to visually separate groups.
- Preview
- Code
import { Dropdown } from '@andrejground/lab';
export default function App() {
return (
<Dropdown>
<Dropdown.Trigger>
<button>Open Menu</button>
</Dropdown.Trigger>
<Dropdown.Menu>
<Dropdown.Section title="Actions">
<Dropdown.Item description="Create a new file">
New file
</Dropdown.Item>
<Dropdown.Item description="Copy the file link">
Copy link
</Dropdown.Item>
</Dropdown.Section>
<Dropdown.Divider />
<Dropdown.Section title="Danger zone">
<Dropdown.Item description="Permanently delete this file">
Delete
</Dropdown.Item>
</Dropdown.Section>
</Dropdown.Menu>
</Dropdown>
);
}
Nested
Use isNested to create submenus. Nested dropdowns open on hover (and/or click) and display a caret indicator.
Keyboard navigation is fully supported between parent and child menus. Use Enter to open a nested menu, and Esc to close it.
- Preview
- Code
import { Dropdown } from '@andrejground/lab';
export default function App() {
return (
<Dropdown>
<Dropdown.Trigger>
<button>Actions</button>
</Dropdown.Trigger>
<Dropdown.Menu>
<Dropdown.Item>New file</Dropdown.Item>
<Dropdown.Item>Copy link</Dropdown.Item>
<Dropdown isNested>
<Dropdown.Trigger>
<Dropdown.Item shouldCloseOnSelection={false}>
Export
</Dropdown.Item>
</Dropdown.Trigger>
<Dropdown.Menu>
<Dropdown.Item>JSON</Dropdown.Item>
<Dropdown.Item>CSV</Dropdown.Item>
<Dropdown.Item>PDF</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</Dropdown.Menu>
</Dropdown>
);
}
Disabled Items
Use the disabled prop on Dropdown.Item to prevent interaction.
- Preview
- Code
import { Dropdown } from '@andrejground/lab';
export default function App() {
return (
<Dropdown>
<Dropdown.Trigger>
<button>Open Menu</button>
</Dropdown.Trigger>
<Dropdown.Menu>
<Dropdown.Item>New file</Dropdown.Item>
<Dropdown.Item disabled>Copy link (disabled)</Dropdown.Item>
<Dropdown.Item>Edit</Dropdown.Item>
<Dropdown.Item disabled>Delete (disabled)</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
);
}
Polymorphic Items
Use the as prop to render items as different elements - links, anchors, or any other component.
- Preview
- Code
import { Dropdown } from '@andrejground/lab';
export default function App() {
return (
<Dropdown>
<Dropdown.Trigger>
<button>Navigation</button>
</Dropdown.Trigger>
<Dropdown.Menu>
<Dropdown.Item as="a" href="/blog" description="Visit the blog">
Blog (Link)
</Dropdown.Item>
<Dropdown.Item as="a" href="https://github.com" target="_blank" description="Open GitHub">
GitHub (anchor)
</Dropdown.Item>
<Dropdown.Item onClick={() => alert('Clicked!')} description="Triggers an action">
Action (div)
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
);
}
Controlled
- Preview
- Code
falseimport React from 'react';
import { Dropdown } from '@andrejground/lab';
export default function App() {
const [isOpen, setIsOpen] = React.useState(false);
return (
<>
<Dropdown isOpen={isOpen} onOpenChange={setIsOpen}>
<Dropdown.Trigger>
<button>Open Menu</button>
</Dropdown.Trigger>
<Dropdown.Menu>
<Dropdown.Item>New file</Dropdown.Item>
<Dropdown.Item>Copy link</Dropdown.Item>
<Dropdown.Item>Edit</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<br />
<div>
Open: <code>{`${isOpen}`}</code>
</div>
</>
);
}
API
Dropdown Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Dropdown content (expects Dropdown.Trigger and Dropdown.Menu) |
trigger | ReactNode | - | Element that triggers the dropdown |
shouldCloseOnSelection | boolean | true | Whether the dropdown closes after an item is selected |
caret | ReactNode | - | Custom caret element replacing the default indicator |
showCaret | boolean | isNested | Shows or hides the caret indicator |
autoFocus | "first-item" | "last-item" | "menu" | "none" | "menu" | Controls which item receives focus when the dropdown opens |
isOpen | boolean | - | Controls the dropdown open state (controlled mode) |
isDisabled | boolean | - | Disables the dropdown trigger |
isNested | boolean | false | Indicates the dropdown is nested inside another dropdown |
placement | PopoverPlacement | isNested ? "right-start" : "bottom-center" | Preferred placement of the dropdown relative to the trigger |
offset | number | - | Distance in pixels between the dropdown and the trigger |
backdrop | "none" | "transparent" | "opaque" | "blur" | - | The backdrop style displayed behind the dropdown |
shouldFlip | boolean | true | Flips placement when there is not enough space |
shouldBlockScroll | boolean | true | Blocks page scroll when the dropdown is open |
shouldCloseOnScroll | boolean | !shouldBlockScroll | Closes the dropdown when the page is scrolled |
shouldCloseOnClickOutside | boolean | true | Closes the dropdown when clicking outside of it |
shouldCloseOnEsc | boolean | true | Closes the dropdown when the Escape key is pressed |
openOnHover | boolean | isNested | Opens the dropdown on hover instead of click |
showArrow | boolean | false | Shows an arrow pointing toward the trigger |
growContent | boolean | - | Allows the dropdown content to grow beyond the trigger width |
onOpen | () => void | - | Callback fired when the dropdown opens |
onClose | () => void | - | Callback fired when the dropdown closes |
onClickOutside | () => void | - | Callback fired when clicking outside the dropdown |
onOpenChange | (isOpen: boolean) => void | - | Callback fired when the open state changes |
onTriggerFocus | () => void | - | Callback fired when the trigger receives focus |
onTriggerBlur | () => void | - | Callback fired when the trigger loses focus |
triggerWrapper | boolean | - | Wraps the trigger in a span instead of using Slot |
fullWidthTriggerWrapper | boolean | - | Makes the trigger wrapper take full width |
focusTrapProps | { trapFocus?: boolean; autoFocus?: boolean } | { trapFocus: true, autoFocus: autoFocus === "none" } | Configuration for focus trap behavior |
classNames | DropdownClassNames | - | Custom class names for the dropdown slots |
Dropdown.Trigger Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Trigger content |
classNames | DropdownTriggerClassNames | - | Custom class names for the trigger slots |
Dropdown.Menu Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Menu content (Dropdown.Section, Dropdown.Item, etc.) |
classNames | DropdownMenuClassNames | - | Custom class names for the menu slots |
Dropdown.Section Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Section content |
scrolling | boolean | - | Enables scrolling within this section |
title | ReactNode | - | Title displayed above the section |
isStickyTitle | boolean | true | Keeps the section title visible while the section scrolls |
infiniteScrollProps | InfiniteScrollProps | - | Configuration for infinite scrolling within this section |
classNames | DropdownSectionClassNames | - | Custom class names for the section slots |
Dropdown.Item Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Item content |
isHighlighted | boolean | - | Visually highlights the item |
shouldCloseOnSelection | boolean | - | Whether selecting this item closes the dropdown |
disabled | boolean | - | Disables the item |
showDisabledStyles | boolean | disabled | Shows disabled visual styles without disabling interaction |
startContent | ReactNode | - | Content rendered before the item text |
endContent | ReactNode | - | Content rendered after the item text |
as | ElementType | "div" | Renders the item as a different HTML element or component |
description | string | - | Description text displayed below the item content |
classNames | DropdownItemClassNames | - | Custom class names for the item slots |
Dropdown.Header Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Header content |
isSticky | boolean | - | Keeps the header visible while the menu scrolls |
classNames | DropdownHeaderClassNames | - | Custom class names for the header slots |
Dropdown.Footer Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Footer content |
isSticky | boolean | - | Keeps the footer visible while the menu scrolls |
classNames | DropdownFooterClassNames | - | Custom class names for the footer slots |
Dropdown.Divider Props
| Prop | Type | Default | Description |
|---|---|---|---|
classNames | DropdownDividerClassNames | - | Custom class names for the divider slots |