Dropdown
A dropdown displays a list of actions or options that a user can choose
Usage
import { Dropdown } from '@lenso/ui';"use client";
// Adapted from HeroUI v3.2.6 dropdown-default (Apache-2.0).import { Button, Dropdown } from "@lenso/ui";import { ActionItem, Popup } from "./_shared";
export function Default() { return ( <Dropdown> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Actions </Dropdown.Trigger> <Popup> <ActionItem label="New file" onClick={() => console.log("Selected: new-file")} /> <ActionItem label="Copy link" onClick={() => console.log("Selected: copy-link")} /> <ActionItem label="Edit file" onClick={() => console.log("Selected: edit-file")} /> <ActionItem label="Delete file" variant="danger" onClick={() => console.log("Selected: delete-file")} /> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Anatomy
import { Dropdown, Button, Label, Description, Header, Kbd, Separator } from '@lenso/ui';
export default () => ( <Dropdown> <Dropdown.Trigger> <Button /> </Dropdown.Trigger> <Dropdown.Popover> <Dropdown.Menu> <Dropdown.Item> <Label /> <Description /> <Kbd slot="keyboard" /> <Dropdown.ItemIndicator /> </Dropdown.Item> <Separator /> <Dropdown.Section> <Header /> <Dropdown.Item /> </Dropdown.Section> <Dropdown.SubmenuTrigger> <Dropdown.Item> <Label /> <Dropdown.SubmenuIndicator /> </Dropdown.Item> <Dropdown.Popover> <Dropdown.Menu> <Dropdown.Item /> </Dropdown.Menu> </Dropdown.Popover> </Dropdown.SubmenuTrigger> </Dropdown.Menu> </Dropdown.Popover> </Dropdown>)Examples
With Icons
"use client";
// Adapted from HeroUI v3.2.6 with-icons, Apache-2.0.import { FloppyDisk, FolderOpen, SquarePlus, TrashBin } from "@gravity-ui/icons";import * as stylex from "@stylexjs/stylex";import { Button, Dropdown } from "@lenso/ui";import { ActionItem, Popup, Shortcut, styles } from "./_shared";
export function WithIcons() { return ( <Dropdown> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Actions </Dropdown.Trigger> <Popup> <ActionItem label="New file" icon={<SquarePlus {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="N" />} onClick={() => console.log("Selected: new-file")} /> <ActionItem label="Open file" icon={<FolderOpen {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="O" />} onClick={() => console.log("Selected: open-file")} /> <ActionItem label="Save file" icon={<FloppyDisk {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="S" />} onClick={() => console.log("Selected: save-file")} /> <ActionItem label="Delete file" variant="danger" icon={<TrashBin {...stylex.props(styles.dangerIcon)} />} shortcut={<Shortcut letter="D" shift />} onClick={() => console.log("Selected: delete-file")} /> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
With Descriptions
"use client";
// Adapted from HeroUI v3.2.6 with-descriptions, Apache-2.0.import { FloppyDisk, FolderOpen, SquarePlus, TrashBin } from "@gravity-ui/icons";import * as stylex from "@stylexjs/stylex";import { Button, Dropdown } from "@lenso/ui";import { ActionItem, Popup, Shortcut, styles } from "./_shared";
export function WithDescriptions() { return ( <Dropdown> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Actions </Dropdown.Trigger> <Popup> <ActionItem label="New file" description="Create a new file" icon={<SquarePlus {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="N" />} onClick={() => console.log("Selected: new-file")} /> <ActionItem label="Open file" description="Open an existing file" icon={<FolderOpen {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="O" />} onClick={() => console.log("Selected: open-file")} /> <ActionItem label="Save file" description="Save the current file" icon={<FloppyDisk {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="S" />} onClick={() => console.log("Selected: save-file")} /> <ActionItem label="Delete file" description="Move to trash" variant="danger" icon={<TrashBin {...stylex.props(styles.dangerIcon)} />} shortcut={<Shortcut letter="D" shift />} onClick={() => console.log("Selected: delete-file")} /> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
With Disabled Items
"use client";
// Adapted from HeroUI v3.2.6 with-disabled-items, Apache-2.0.import { Bars, Pencil, SquarePlus, TrashBin } from "@gravity-ui/icons";import * as stylex from "@stylexjs/stylex";import { Button, Dropdown } from "@lenso/ui";import { ActionItem, Popup, Shortcut, styles } from "./_shared";
export function WithDisabledItems() { return ( <Dropdown> <Dropdown.Trigger render={<Button isIconOnly aria-label="Menu" variant="secondary" />}> <Bars /> </Dropdown.Trigger> <Popup xstyle={styles.disabledWidth}> <Dropdown.Section> <Dropdown.Section.Label>Actions</Dropdown.Section.Label> <ActionItem label="New file" description="Create a new file" icon={<SquarePlus {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="N" />} onClick={() => console.log("Selected: new-file")} /> <ActionItem label="Edit file" description="Make changes" icon={<Pencil {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="E" />} onClick={() => console.log("Selected: edit-file")} /> </Dropdown.Section> <Dropdown.Separator /> <Dropdown.Section> <Dropdown.Section.Label>Danger zone</Dropdown.Section.Label> <ActionItem disabled label="Delete file" description="Move to trash" variant="danger" icon={<TrashBin {...stylex.props(styles.dangerIcon)} />} shortcut={<Shortcut letter="D" shift />} onClick={() => console.log("Selected: delete-file")} /> </Dropdown.Section> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
With Sections
"use client";
// Adapted from HeroUI v3.2.6 with-sections, Apache-2.0.import { EllipsisVertical, Pencil, SquarePlus, TrashBin } from "@gravity-ui/icons";import * as stylex from "@stylexjs/stylex";import { Button, Dropdown } from "@lenso/ui";import { ActionItem, Popup, Shortcut, styles } from "./_shared";
export function WithSections() { return ( <Dropdown> <Dropdown.Trigger render={<Button isIconOnly aria-label="Menu" variant="secondary" />}> <EllipsisVertical /> </Dropdown.Trigger> <Popup> <Dropdown.Section> <Dropdown.Section.Label>Actions</Dropdown.Section.Label> <ActionItem label="New file" description="Create a new file" icon={<SquarePlus {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="N" />} onClick={() => console.log("Selected: new-file")} /> <ActionItem label="Edit file" description="Make changes" icon={<Pencil {...stylex.props(styles.icon)} />} shortcut={<Shortcut letter="E" />} onClick={() => console.log("Selected: edit-file")} /> </Dropdown.Section> <Dropdown.Separator /> <Dropdown.Section> <Dropdown.Section.Label>Danger zone</Dropdown.Section.Label> <ActionItem label="Delete file" description="Move to trash" variant="danger" icon={<TrashBin {...stylex.props(styles.dangerIcon)} />} shortcut={<Shortcut letter="D" shift />} onClick={() => console.log("Selected: delete-file")} /> </Dropdown.Section> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
With Multiple Selection
"use client";
// Adapted from HeroUI v3.2.6 with-multiple-selection, Apache-2.0.import { useState } from "react";import { Button, Dropdown, MenuItem } from "@lenso/ui";import { Checkmark, Popup, styles } from "./_shared";
export function WithMultipleSelection() { const [selected, setSelected] = useState(new Set(["apple"])); const [open, setOpen] = useState(false); const fruit = (value: string, label: string) => ( <Dropdown.CheckboxItem key={value} checked={selected.has(value)} closeOnClick={false} onKeyDown={(event) => { if (event.key === "Enter") setOpen(false); }} onCheckedChange={(checked) => setSelected((previous) => { const next = new Set(previous); if (checked) next.add(value); else next.delete(value); return next; }) } > <Dropdown.CheckboxItemIndicator keepMounted xstyle={styles.selectionIndicator}> <Checkmark /> </Dropdown.CheckboxItemIndicator> <MenuItem.Label>{label}</MenuItem.Label> </Dropdown.CheckboxItem> ); return ( <Dropdown open={open} onOpenChange={setOpen}> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Preferred Fruits </Dropdown.Trigger> <Popup xstyle={styles.wide}> <Dropdown.Section> <Dropdown.Section.Label>Select a fruit</Dropdown.Section.Label> {fruit("apple", "Apple")} {fruit("banana", "Banana")} {fruit("cherry", "Cherry")} </Dropdown.Section> {fruit("orange", "Orange")} {fruit("pear", "Pear")} </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Controlled
"use client";
// Adapted from HeroUI v3.2.6 controlled, Apache-2.0.import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { Button, Dropdown, MenuItem } from "@lenso/ui";import { Checkmark, Popup, styles } from "./_shared";
export function Controlled() { const [selected, setSelected] = useState(new Set(["bold"])); const [open, setOpen] = useState(false); const selectedItems = Array.from(selected); return ( <div {...stylex.props(styles.controlled)}> <p {...stylex.props(styles.status)}> Selected: {selectedItems.length > 0 ? selectedItems.join(", ") : "None"} </p> <Dropdown open={open} onOpenChange={setOpen}> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Actions </Dropdown.Trigger> <Popup> {(["bold", "italic", "underline"] as const).map((value) => ( <Dropdown.CheckboxItem key={value} checked={selected.has(value)} closeOnClick={false} onKeyDown={(event) => { if (event.key === "Enter") setOpen(false); }} onCheckedChange={(checked) => setSelected((previous) => { const next = new Set(previous); if (checked) next.add(value); else next.delete(value); return next; }) } > <MenuItem.Label>{value.charAt(0).toUpperCase() + value.slice(1)}</MenuItem.Label> <Dropdown.CheckboxItemIndicator keepMounted xstyle={styles.selectionIndicator}> <Checkmark /> </Dropdown.CheckboxItemIndicator> </Dropdown.CheckboxItem> ))} </Popup> </Dropdown> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Controlled Open State
"use client";
// Adapted from HeroUI v3.2.6 controlled-open-state, Apache-2.0.import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { Button, Dropdown } from "@lenso/ui";import { ActionItem, Popup, styles } from "./_shared";
export function ControlledOpenState() { const [open, setOpen] = useState(false); return ( <div {...stylex.props(styles.controlled)}> <p {...stylex.props(styles.status)}> Dropdown is: <strong>{open ? "open" : "closed"}</strong> </p> <Dropdown open={open} onOpenChange={setOpen}> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Actions </Dropdown.Trigger> <Popup> <ActionItem label="New file" /> <ActionItem label="Open file" /> <ActionItem label="Save file" /> <ActionItem label="Delete file" variant="danger" /> </Popup> </Dropdown> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
With Single Selection
"use client";
// Adapted from HeroUI v3.2.6 with-single-selection, Apache-2.0.import { useState } from "react";import { Button, Dropdown, MenuItem } from "@lenso/ui";import { Checkmark, Popup, styles } from "./_shared";
export function WithSingleSelection() { const [selected, setSelected] = useState("apple"); const fruit = (value: string, label: string) => ( <Dropdown.RadioItem key={value} value={value} closeOnClick> <Dropdown.RadioItemIndicator keepMounted xstyle={styles.selectionIndicator}> <Checkmark /> </Dropdown.RadioItemIndicator> <MenuItem.Label>{label}</MenuItem.Label> </Dropdown.RadioItem> ); return ( <Dropdown> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Fruit </Dropdown.Trigger> <Popup xstyle={styles.wide}> <Dropdown.RadioGroup value={selected} onValueChange={setSelected}> <Dropdown.Section> <Dropdown.Section.Label>Select a fruit</Dropdown.Section.Label> {fruit("apple", "Apple")} {fruit("banana", "Banana")} {fruit("cherry", "Cherry")} </Dropdown.Section> {fruit("orange", "Orange")} {fruit("pear", "Pear")} </Dropdown.RadioGroup> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Single With Custom Indicator
"use client";
// Adapted from HeroUI v3.2.6 single-with-custom-indicator, Apache-2.0.import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { Button, Dropdown, MenuItem } from "@lenso/ui";import { Popup, styles } from "./_shared";
export function SingleWithCustomIndicator() { const [selected, setSelected] = useState("apple"); const customCheckmarkIcon = ( <svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" > <path {...stylex.props(styles.accent)} clipRule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14m3.1-8.55a.75.75 0 1 0-1.2-.9L7.419 8.858L6.03 7.47a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.13-.08z" fill="currentColor" fillRule="evenodd" /> </svg> ); const fruit = (value: string, label: string) => ( <Dropdown.RadioItem key={value} value={value} closeOnClick> <Dropdown.RadioItemIndicator keepMounted xstyle={styles.selectionIndicator}> {customCheckmarkIcon} </Dropdown.RadioItemIndicator> <MenuItem.Label>{label}</MenuItem.Label> </Dropdown.RadioItem> ); return ( <Dropdown> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Fruits </Dropdown.Trigger> <Popup xstyle={styles.wide}> <Dropdown.RadioGroup value={selected} onValueChange={setSelected}> <Dropdown.Section> <Dropdown.Section.Label>Select a fruit</Dropdown.Section.Label> {fruit("apple", "Apple")} {fruit("banana", "Banana")} {fruit("cherry", "Cherry")} </Dropdown.Section> {fruit("orange", "Orange")} {fruit("pear", "Pear")} </Dropdown.RadioGroup> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
With Section Level Selection
"use client";
// Adapted from HeroUI v3.2.6 with-section-level-selection, Apache-2.0.import { useState } from "react";import { Button, Dropdown, MenuItem } from "@lenso/ui";import { ActionItem, Checkmark, Dotmark, Popup, Shortcut, styles } from "./_shared";
export function WithSectionLevelSelection() { const [textStyles, setTextStyles] = useState(new Set(["bold", "italic"])); const [textAlignment, setTextAlignment] = useState("left"); const [open, setOpen] = useState(false); return ( <Dropdown open={open} onOpenChange={setOpen}> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Styles </Dropdown.Trigger> <Popup xstyle={styles.wide}> <Dropdown.Section> <Dropdown.Section.Label>Actions</Dropdown.Section.Label> <ActionItem label="Cut" shortcut={<Shortcut letter="X" />} /> <ActionItem label="Copy" shortcut={<Shortcut letter="C" />} /> <ActionItem label="Paste" shortcut={<Shortcut letter="U" />} /> </Dropdown.Section> <Dropdown.Separator /> <Dropdown.Section> <Dropdown.Section.Label>Text Style</Dropdown.Section.Label> {(["bold", "italic", "underline"] as const).map((value) => ( <Dropdown.CheckboxItem key={value} checked={textStyles.has(value)} aria-label={value.charAt(0).toUpperCase() + value.slice(1)} closeOnClick={false} onKeyDown={(event) => { if (event.key === "Enter") setOpen(false); }} onCheckedChange={(checked) => setTextStyles((previous) => { const next = new Set(previous); if (checked) next.add(value); else next.delete(value); return next; }) } > <Dropdown.CheckboxItemIndicator keepMounted xstyle={styles.selectionIndicator}> <Checkmark /> </Dropdown.CheckboxItemIndicator> <MenuItem.Label>{value.charAt(0).toUpperCase() + value.slice(1)}</MenuItem.Label> <Shortcut letter={value === "underline" ? "U" : value.charAt(0).toUpperCase()} /> </Dropdown.CheckboxItem> ))} </Dropdown.Section> <Dropdown.Separator /> <Dropdown.Section> <Dropdown.Section.Label>Text Alignment</Dropdown.Section.Label> <Dropdown.RadioGroup value={textAlignment} onValueChange={setTextAlignment}> {(["left", "center", "right"] as const).map((value) => ( <Dropdown.RadioItem key={value} value={value} aria-label={value.charAt(0).toUpperCase() + value.slice(1)} closeOnClick > <Dropdown.RadioItemIndicator keepMounted xstyle={styles.selectionIndicator}> <Dotmark /> </Dropdown.RadioItemIndicator> <MenuItem.Label>{value.charAt(0).toUpperCase() + value.slice(1)}</MenuItem.Label> <Shortcut modifier="alt" letter={value === "left" ? "A" : value === "center" ? "H" : "D"} /> </Dropdown.RadioItem> ))} </Dropdown.RadioGroup> </Dropdown.Section> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
With Keyboard Shortcuts
"use client";
// Adapted from HeroUI v3.2.6 with-keyboard-shortcuts, Apache-2.0.import { Button, Dropdown } from "@lenso/ui";import { ActionItem, Popup, Shortcut } from "./_shared";
export function WithKeyboardShortcuts() { return ( <Dropdown> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Actions </Dropdown.Trigger> <Popup> <ActionItem label="New" shortcut={<Shortcut letter="N" />} onClick={() => console.log("Selected: new")} /> <ActionItem label="Open" shortcut={<Shortcut letter="O" />} onClick={() => console.log("Selected: open")} /> <ActionItem label="Save" shortcut={<Shortcut letter="S" />} onClick={() => console.log("Selected: save")} /> <ActionItem label="Delete" variant="danger" shortcut={<Shortcut letter="D" shift />} onClick={() => console.log("Selected: delete")} /> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
With Submenus
"use client";
// Adapted from HeroUI v3.2.6 with-submenus, Apache-2.0.import * as stylex from "@stylexjs/stylex";import { Button, Dropdown, IconChevronRight, MenuItem } from "@lenso/ui";import { ActionItem, Popup, styles } from "./_shared";
export function WithSubmenus() { return ( <Dropdown> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Share </Dropdown.Trigger> <Popup> <ActionItem label="Copy Link" onClick={() => console.log("Selected: copy-link")} /> <ActionItem label="Facebook" onClick={() => console.log("Selected: facebook")} /> <ActionItem label="X / Twitter" navigationLabel="Twitter" onClick={() => console.log("Selected: twitter")} /> <Dropdown.SubmenuRoot> <Dropdown.SubmenuTrigger label="Share"> <MenuItem.Label>Other</MenuItem.Label> <MenuItem.SubmenuIndicator> <IconChevronRight {...stylex.props(styles.smallIcon)} /> </MenuItem.SubmenuIndicator> </Dropdown.SubmenuTrigger> <Popup> <ActionItem label="WhatsApp" /> <ActionItem label="Telegram" /> <ActionItem label="Discord" /> <Dropdown.SubmenuRoot> <Dropdown.SubmenuTrigger> <MenuItem.Label>Email</MenuItem.Label> <MenuItem.SubmenuIndicator> <IconChevronRight {...stylex.props(styles.smallIcon)} /> </MenuItem.SubmenuIndicator> </Dropdown.SubmenuTrigger> <Popup> <ActionItem label="Work email" /> <ActionItem label="Personal email" /> </Popup> </Dropdown.SubmenuRoot> </Popup> </Dropdown.SubmenuRoot> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
With Custom Submenu Indicator
"use client";
// Adapted from HeroUI v3.2.6 with-custom-submenu-indicator, Apache-2.0.import { ArrowRight } from "@gravity-ui/icons";import * as stylex from "@stylexjs/stylex";import { Button, Dropdown, IconChevronRight, MenuItem } from "@lenso/ui";import { ActionItem, Popup, styles } from "./_shared";
export function WithCustomSubmenuIndicator() { return ( <Dropdown> <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />}> Share </Dropdown.Trigger> <Popup> <ActionItem label="Copy Link" onClick={() => console.log("Selected: copy-link")} /> <ActionItem label="Facebook" onClick={() => console.log("Selected: facebook")} /> <Dropdown.SubmenuRoot> <Dropdown.SubmenuTrigger label="Share"> <MenuItem.Label>More options</MenuItem.Label> <MenuItem.SubmenuIndicator> <ArrowRight {...stylex.props(styles.smallIcon)} /> </MenuItem.SubmenuIndicator> </Dropdown.SubmenuTrigger> <Popup> <ActionItem label="WhatsApp" /> <ActionItem label="Telegram" /> <Dropdown.SubmenuRoot> <Dropdown.SubmenuTrigger> <MenuItem.Label>Email</MenuItem.Label> <MenuItem.SubmenuIndicator> <svg {...stylex.props(styles.smallIcon)} fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" aria-hidden="true" > <path d="M9 18l6-6-6-6" /> </svg> </MenuItem.SubmenuIndicator> </Dropdown.SubmenuTrigger> <Popup> <ActionItem label="Work email" /> <ActionItem label="Personal email" /> </Popup> </Dropdown.SubmenuRoot> <ActionItem label="Discord" /> </Popup> </Dropdown.SubmenuRoot> <Dropdown.SubmenuRoot> <Dropdown.SubmenuTrigger label="Other"> <MenuItem.Label>Other (default indicator)</MenuItem.Label> <MenuItem.SubmenuIndicator> <IconChevronRight {...stylex.props(styles.smallIcon)} /> </MenuItem.SubmenuIndicator> </Dropdown.SubmenuTrigger> <Popup> <ActionItem label="SMS" /> </Popup> </Dropdown.SubmenuRoot> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Custom Trigger
"use client";
// Adapted from HeroUI v3.2.6 custom-trigger, Apache-2.0; original avatar asset retained.import { ArrowRightFromSquare, Gear, Persons } from "@gravity-ui/icons";import * as stylex from "@stylexjs/stylex";import { Avatar, Dropdown, MenuItem } from "@lenso/ui";import { ActionItem, Popup, styles } from "./_shared";
const avatar = "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/orange.jpg";
export function CustomTrigger() { return ( <Dropdown> <Dropdown.Trigger xstyle={styles.round} aria-label="Account menu"> <Avatar> <Avatar.Image alt="Junior Garcia" src={avatar} /> <Avatar.Fallback delay={600}>JD</Avatar.Fallback> </Avatar> </Dropdown.Trigger> <Popup> <div {...stylex.props(styles.profile)}> <div {...stylex.props(styles.profileRow)}> <Avatar size="sm"> <Avatar.Image alt="Jane" src={avatar} /> <Avatar.Fallback delay={600}>JD</Avatar.Fallback> </Avatar> <div {...stylex.props(styles.text)}> <p {...stylex.props(styles.profileName)}>Jane Doe</p> <p {...stylex.props(styles.profileEmail)}>[email protected]</p> </div> </div> </div> <ActionItem label="Dashboard" /> <ActionItem label="Profile" /> <Dropdown.Item label="Settings"> <div {...stylex.props(styles.itemRow)}> <MenuItem.Label>Settings</MenuItem.Label> <Gear {...stylex.props(styles.smallIcon)} /> </div> </Dropdown.Item> <Dropdown.Item label="New project"> <div {...stylex.props(styles.itemRow)}> <MenuItem.Label>Create Team</MenuItem.Label> <Persons {...stylex.props(styles.smallIcon)} /> </div> </Dropdown.Item> <Dropdown.Item label="Logout" variant="danger"> <div {...stylex.props(styles.itemRow)}> <MenuItem.Label>Log Out</MenuItem.Label> <ArrowRightFromSquare {...stylex.props(styles.smallDangerIcon)} /> </div> </Dropdown.Item> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Long Press Trigger
"use client";
// Adapted from HeroUI v3.2.6 long-press-trigger, Apache-2.0.import { useEffect, useRef, useState } from "react";import { Button, Dropdown } from "@lenso/ui";import { ActionItem, Popup } from "./_shared";
export function LongPressTrigger() { const [open, setOpen] = useState(false); const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined); const pointerActive = useRef(false); const cancel = () => { clearTimeout(timer.current); timer.current = undefined; }; useEffect(() => () => clearTimeout(timer.current), []); return ( <Dropdown open={open} onOpenChange={(next, details) => { if (details.reason === "trigger-press" && pointerActive.current) return; setOpen(next); }} > <Dropdown.Trigger render={<Button aria-label="Menu" variant="secondary" />} onPointerDown={(event) => { if (event.button !== 0) return; pointerActive.current = true; cancel(); timer.current = setTimeout(() => setOpen(true), 500); }} onPointerUp={cancel} onPointerCancel={cancel} onPointerLeave={cancel} onKeyDown={() => { pointerActive.current = false; }} > Long Press </Dropdown.Trigger> <Popup> <ActionItem label="New file" /> <ActionItem label="Open file" /> <ActionItem label="Save file" /> <ActionItem label="Delete file" variant="danger" /> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Customization
Tailwind CSS
"use client";
// Adapted from HeroUI v3.2.6 custom-styles, Apache-2.0.import { Button, Dropdown } from "@lenso/ui";import { ActionItem, Popup, styles } from "./_shared";
export function CustomStyles() { return ( <Dropdown> <Dropdown.Trigger render={<Button variant="secondary" />}>Actions</Dropdown.Trigger> <Popup xstyle={styles.customPopup}> <ActionItem label="Rename" xstyle={styles.customItem} /> <ActionItem label="Duplicate" xstyle={styles.customItem} /> <ActionItem label="Delete" variant="danger" xstyle={styles.customDangerItem} /> </Popup> </Dropdown> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Global CSS
To customize the Dropdown component classes, you can use the @layer components directive.
Learn more.
@layer components { .dropdown { @apply flex flex-col gap-1; }
.dropdown__trigger { @apply outline-none; }
.dropdown__popover { @apply rounded-lg border border-border bg-overlay p-2; }
.dropdown__menu { @apply flex flex-col gap-1; }}Styling Reference
HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.
CSS Classes
The Dropdown component uses these CSS classes (View source styles):
Base Classes [!toc]
.dropdown- Base dropdown container.dropdown__trigger- The button or element that triggers the dropdown.dropdown__popover- The popover container.dropdown__menu- The menu container inside the popover
State Classes [!toc]
.dropdown__trigger[data-focus-visible="true"]- Focused trigger state.dropdown__trigger[data-disabled="true"]- Disabled trigger state.dropdown__trigger[data-pressed="true"]- Pressed trigger state.dropdown__popover[data-entering]- Entering animation state.dropdown__popover[data-exiting]- Exiting animation state.dropdown__menu[data-selection-mode="single"]- Single selection mode.dropdown__menu[data-selection-mode="multiple"]- Multiple selection mode
Menu Component Classes
The Dropdown component uses Menu, MenuItem, and MenuSection as base components. These classes are also available for customization:
Menu Classes [!toc]
.menu- Base menu container (menu.css)[data-slot="separator"]- Separator elements within the menu
MenuItem Classes [!toc]
.menu-item- Base menu item container (menu-item.css).menu-item__indicator- Selection indicator (checkmark or dot)[data-slot="menu-item-indicator--checkmark"]- Checkmark indicator SVG[data-slot="menu-item-indicator--dot"]- Dot indicator SVG
.menu-item__indicator--submenu- Submenu indicator (chevron).menu-item--default- Default variant styling.menu-item--danger- Danger variant styling
MenuItem State Classes [!toc]
.menu-item[data-focus-visible="true"]- Focused item state (keyboard focus).menu-item[data-focus="true"]- Focused item state.menu-item[data-pressed]- Pressed item state.menu-item[data-hovered]- Hovered item state.menu-item[data-selected="true"]- Selected item state.menu-item[data-disabled]- Disabled item state.menu-item[data-has-submenu="true"]- Item with submenu.menu-item[data-selection-mode="single"]- Single selection mode.menu-item[data-selection-mode="multiple"]- Multiple selection mode.menu-item[aria-checked="true"]- Checked item (ARIA).menu-item[aria-selected="true"]- Selected item (ARIA)
MenuSection Classes [!toc]
.menu-section- Base menu section container (menu-section.css)
Interactive States
The component supports both CSS pseudo-classes and data attributes for flexibility:
- Hover:
:hoveror[data-hovered="true"]on trigger and items - Focus:
:focus-visibleor[data-focus-visible="true"]on trigger and items - Disabled:
:disabledor[data-disabled="true"]on trigger and items - Pressed:
:activeor[data-pressed="true"]on trigger and items - Selected:
[data-selected="true"]or[aria-selected="true"]on items
API Reference
Dropdown
| Prop | Type | Default | Description |
|---|---|---|---|
isOpen | boolean | - | Sets the open state of the menu (controlled) |
defaultOpen | boolean | - | Sets the default open state of the menu (uncontrolled) |
onOpenChange | (isOpen: boolean) => void | - | Handler called when the open state changes |
trigger | "press" | "longPress" | "press" | The type of interaction that triggers the menu |
className | string | - | Additional CSS classes |
children | ReactNode | - | Dropdown content |
Dropdown.Trigger
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | ReactNode | RenderFunction | - | Trigger content or render function |
All Button props are also supported when using a Button as the trigger.
Dropdown.Popover
| Prop | Type | Default | Description |
|---|---|---|---|
placement | "bottom" | "bottom left" | "bottom right" | "bottom start" | "bottom end" | "top" | "top left" | "top right" | "top start" | "top end" | "left" | "left top" | "left bottom" | "start" | "start top" | "start bottom" | "right" | "right top" | "right bottom" | "end" | "end top" | "end bottom" | "bottom" | Placement of the popover relative to the trigger |
className | string | - | Additional CSS classes |
children | ReactNode | - | Content children |
All Popover props are also supported.
Dropdown.Menu
| Prop | Type | Default | Description |
|---|---|---|---|
selectionMode | "single" | "multiple" | "none" | "none" | Whether single or multiple selection is enabled |
selectedKeys | Iterable<Key> | - | The currently selected keys (controlled) |
defaultSelectedKeys | Iterable<Key> | - | The initial selected keys (uncontrolled) |
onSelectionChange | (keys: Selection) => void | - | Handler called when the selection changes |
disabledKeys | Iterable<Key> | - | Keys of disabled items |
onAction | (key: Key) => void | - | Handler called when an item is activated |
className | string | - | Additional CSS classes |
children | ReactNode | - | Menu content |
All Menu props are also supported.
Dropdown.Section
| Prop | Type | Default | Description |
|---|---|---|---|
selectionMode | "single" | "multiple" | - | Selection mode for items within this section |
selectedKeys | Iterable<Key> | - | The currently selected keys (controlled) |
defaultSelectedKeys | Iterable<Key> | - | The initial selected keys (uncontrolled) |
onSelectionChange | (keys: Selection) => void | - | Handler called when the selection changes |
disabledKeys | Iterable<Key> | - | Keys of disabled items |
className | string | - | Additional CSS classes |
children | ReactNode | - | Section content |
All MenuSection props are also supported.
Dropdown.Item
| Prop | Type | Default | Description |
|---|---|---|---|
id | Key | - | Unique identifier for the item |
textValue | string | - | Text content of the item for typeahead |
variant | "default" | "danger" | "default" | Visual variant of the item |
className | string | - | Additional CSS classes |
children | ReactNode | RenderFunction | - | Item content or render function |
All MenuItem props are also supported.
Dropdown.ItemIndicator
| Prop | Type | Default | Description |
|---|---|---|---|
type | "checkmark" | "dot" | "checkmark" | Type of indicator to display |
className | string | - | Additional CSS classes |
children | ReactNode | RenderFunction | - | Custom indicator content or render function |
When using a render function, these values are provided:
| Prop | Type | Description |
|---|---|---|
isSelected | boolean | Whether the item is selected |
isIndeterminate | boolean | Whether the item is in an indeterminate state |
Dropdown.SubmenuIndicator
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | ReactNode | - | Custom indicator content |
Dropdown.SubmenuTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | ReactNode | - | Submenu trigger content |
All SubmenuTrigger props are also supported.
Render Props
When using render functions with Dropdown.Item, these values are provided:
| Prop | Type | Description |
|---|---|---|
isSelected | boolean | Whether the item is selected |
isFocused | boolean | Whether the item is focused |
isDisabled | boolean | Whether the item is disabled |
isPressed | boolean | Whether the item is being pressed |
Examples
Basic Usage
import { Dropdown, Button, Label } from '@lenso/ui';
<Dropdown> <Button aria-label="Menu" variant="secondary"> Actions </Button> <Dropdown.Popover> <Dropdown.Menu onAction={(key) => alert(`Selected: ${key}`)}> <Dropdown.Item id="new-file" textValue="New file"> <Label>New file</Label> </Dropdown.Item> <Dropdown.Item id="open-file" textValue="Open file"> <Label>Open file</Label> </Dropdown.Item> <Dropdown.Item id="delete-file" textValue="Delete file" variant="danger"> <Label>Delete file</Label> </Dropdown.Item> </Dropdown.Menu> </Dropdown.Popover></Dropdown>With Sections
import { Dropdown, Button, Label, Header, Separator } from '@lenso/ui';
<Dropdown> <Button aria-label="Menu" variant="secondary"> Actions </Button> <Dropdown.Popover> <Dropdown.Menu onAction={(key) => alert(`Selected: ${key}`)}> <Dropdown.Section> <Header>Actions</Header> <Dropdown.Item id="new-file" textValue="New file"> <Label>New file</Label> </Dropdown.Item> <Dropdown.Item id="edit-file" textValue="Edit file"> <Label>Edit file</Label> </Dropdown.Item> </Dropdown.Section> <Separator /> <Dropdown.Section> <Header>Danger zone</Header> <Dropdown.Item id="delete-file" textValue="Delete file" variant="danger"> <Label>Delete file</Label> </Dropdown.Item> </Dropdown.Section> </Dropdown.Menu> </Dropdown.Popover></Dropdown>Controlled Selection
import type { Selection } from '@lenso/ui';
import { Dropdown, Button, Label } from '@lenso/ui';import { useState } from 'react';
function ControlledDropdown() { const [selected, setSelected] = useState<Selection>(new Set(['bold']));
return ( <Dropdown> <Button aria-label="Menu" variant="secondary"> Actions </Button> <Dropdown.Popover> <Dropdown.Menu selectedKeys={selected} selectionMode="multiple" onSelectionChange={setSelected} > <Dropdown.Item id="bold" textValue="Bold"> <Label>Bold</Label> <Dropdown.ItemIndicator /> </Dropdown.Item> <Dropdown.Item id="italic" textValue="Italic"> <Label>Italic</Label> <Dropdown.ItemIndicator /> </Dropdown.Item> </Dropdown.Menu> </Dropdown.Popover> </Dropdown> );}With Submenus
import { Dropdown, Button, Label } from '@lenso/ui';
<Dropdown> <Button aria-label="Menu" variant="secondary"> Share </Button> <Dropdown.Popover> <Dropdown.Menu onAction={(key) => alert(`Selected: ${key}`)}> <Dropdown.Item id="copy-link" textValue="Copy Link"> <Label>Copy Link</Label> </Dropdown.Item> <Dropdown.SubmenuTrigger> <Dropdown.Item id="share" textValue="Share"> <Label>Other</Label> <Dropdown.SubmenuIndicator /> </Dropdown.Item> <Dropdown.Popover> <Dropdown.Menu> <Dropdown.Item id="whatsapp" textValue="WhatsApp"> <Label>WhatsApp</Label> </Dropdown.Item> <Dropdown.Item id="telegram" textValue="Telegram"> <Label>Telegram</Label> </Dropdown.Item> </Dropdown.Menu> </Dropdown.Popover> </Dropdown.SubmenuTrigger> </Dropdown.Menu> </Dropdown.Popover></Dropdown>Accessibility
The Dropdown component implements the ARIA menu pattern and provides:
- Full keyboard navigation support (arrow keys, home/end, typeahead)
- Screen reader announcements for actions and selection changes
- Proper focus management
- Support for disabled states
- Long press interaction support
- Submenu navigation
For more information, see the React Aria Menu documentation.