Skip to main content

Dropdown

Source ↗

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, and Dropdown.Footer.
  • Nestable: Create nested submenus by nesting Dropdown components - nesting is auto-detected via context. Nested dropdowns open on hover with a caret indicator.
  • Polymorphic Items: Render items as any element (div, a, Link, etc.) using the as prop.
  • 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.Trigger
  • Dropdown.Menu
  • Dropdown.Item
  • Dropdown.Section
  • Dropdown.Divider
  • Dropdown.Header
  • Dropdown.Footer

Customization

Add your styles and your component is ready to be used.

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;

Usage

Sections

Use Dropdown.Section to group items with titles, and Dropdown.Divider to visually separate groups.

Nested

Nesting is automatic - simply place a Dropdown inside another Dropdown.Menu. The component detects its nesting level via context and adjusts placement, hover behavior, and caret visibility accordingly.

Keyboard navigation is fully supported between parent and child menus. Use Enter to open a nested menu, and Esc to close it.

When a nested dropdown lives inside another overlay (such as a Popover), closing the outer overlay closes nested menus without stealing focus from the outer trigger. Only the root popover restores focus on an outside click.

Disabled Items

Use the disabled prop on Dropdown.Item to prevent interaction.

Infinite Scroll

Use infiniteScrollProps on Dropdown.Section to lazily load items as the user scrolls. The section needs the scrolling prop to enable a scrollable container.

Loading...

Polymorphic Items

Use the as prop to render items as different elements - links, anchors, or any other component.

Controlled

Open: false

API

PropTypeDefaultDescription
childrenReactNode-Dropdown content (expects Dropdown.Trigger and Dropdown.Menu)
triggerReactNode-Element that triggers the dropdown
shouldCloseOnSelectionbooleantrueWhether the dropdown closes after an item is selected
caretReactNode-Custom caret element replacing the default indicator
showCaretbooleanfalse (root) / true (nested)Shows or hides the caret indicator
autoFocus"first-item" | "last-item" | "menu" | "none""menu"Controls which item receives focus when the dropdown opens
isOpenboolean-Controls the dropdown open state (controlled mode)
isDisabledboolean-Disables the dropdown trigger
placementPopoverPlacement"bottom-center" (root) / "right-start" (nested)Preferred placement of the dropdown relative to the trigger
offsetnumber-Distance in pixels between the dropdown and the trigger
backdrop"none" | "transparent" | "opaque" | "blur"-The backdrop style displayed behind the dropdown
shouldFlipbooleantrueFlips placement when there is not enough space
shouldBlockScrollbooleantrueBlocks page scroll when the dropdown is open
shouldCloseOnScrollboolean!shouldBlockScrollCloses the dropdown when the page is scrolled
shouldCloseOnClickOutsidebooleantrueCloses the dropdown when clicking outside of it
shouldCloseOnEscbooleantrueCloses the dropdown when the Escape key is pressed
openOnHoverbooleanfalse (root) / true (nested)Opens the dropdown on hover instead of click
showArrowbooleanfalseShows an arrow pointing toward the trigger
growContentboolean-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(event: MouseEvent | TouchEvent) => void-Callback fired when clicking outside the dropdown
onOpenChange(isOpen: boolean) => void-Callback fired when the open state changes
focusTriggerOnClosebooleantrueReturns focus to the dropdown trigger when the menu closes after a selection (handleCloseRoot). Passed through to the underlying Popover
onTriggerFocus() => void-Callback fired when the trigger receives focus
onTriggerBlur() => void-Callback fired when the trigger loses focus
triggerWrapperboolean-Wraps the trigger in a span instead of using Slot
fullWidthTriggerWrapperboolean-Makes the trigger wrapper take full width
focusTrapProps{ trapFocus?: boolean; autoFocus?: boolean }{ trapFocus: true, autoFocus: autoFocus === "none" }Configuration for focus trap behavior
classNamesDropdownClassNames-Custom class names for the dropdown slots
PropTypeDefaultDescription
childrenReactNode-Trigger content
classNamesDropdownTriggerClassNames-Custom class names for the trigger slots
PropTypeDefaultDescription
childrenReactNode-Menu content (Dropdown.Section, Dropdown.Item, etc.)
classNamesDropdownMenuClassNames-Custom class names for the menu slots
PropTypeDefaultDescription
childrenReactNode-Section content
scrollingboolean-Enables scrolling within this section
titleReactNode-Title displayed above the section
isStickyTitlebooleantrueKeeps the section title visible while the section scrolls
infiniteScrollPropsInfiniteScrollProps-Configuration for infinite scrolling within this section
classNamesDropdownSectionClassNames-Custom class names for the section slots
PropTypeDefaultDescription
childrenReactNode-Item content
isHighlightedboolean-Visually highlights the item
shouldCloseOnSelectionboolean-Whether selecting this item closes the dropdown
disabledboolean-Disables the item
showDisabledStylesbooleandisabledShows disabled visual styles without disabling interaction
startContentReactNode-Content rendered before the item text
endContentReactNode-Content rendered after the item text
asElementType"div"Renders the item as a different HTML element or component
descriptionstring-Description text displayed below the item content
classNamesDropdownItemClassNames-Custom class names for the item slots
PropTypeDefaultDescription
childrenReactNode-Header content
isStickyboolean-Keeps the header visible while the menu scrolls
classNamesDropdownHeaderClassNames-Custom class names for the header slots
PropTypeDefaultDescription
childrenReactNode-Footer content
isStickyboolean-Keeps the footer visible while the menu scrolls
classNamesDropdownFooterClassNames-Custom class names for the footer slots
PropTypeDefaultDescription
classNamesDropdownDividerClassNames-Custom class names for the divider slots