Skip to content
Lenso UI

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

The Dropdown component uses Menu, MenuItem, and MenuSection as base components. These classes are also available for customization:

  • .menu - Base menu container (menu.css)
    • [data-slot="separator"] - Separator elements within the menu
  • .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
  • .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)

Interactive States

The component supports both CSS pseudo-classes and data attributes for flexibility:

  • Hover: :hover or [data-hovered="true"] on trigger and items
  • Focus: :focus-visible or [data-focus-visible="true"] on trigger and items
  • Disabled: :disabled or [data-disabled="true"] on trigger and items
  • Pressed: :active or [data-pressed="true"] on trigger and items
  • Selected: [data-selected="true"] or [aria-selected="true"] on items

API Reference

PropTypeDefaultDescription
isOpenboolean-Sets the open state of the menu (controlled)
defaultOpenboolean-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
classNamestring-Additional CSS classes
childrenReactNode-Dropdown content
PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReactNode | RenderFunction-Trigger content or render function

All Button props are also supported when using a Button as the trigger.

PropTypeDefaultDescription
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
classNamestring-Additional CSS classes
childrenReactNode-Content children

All Popover props are also supported.

PropTypeDefaultDescription
selectionMode"single" | "multiple" | "none""none"Whether single or multiple selection is enabled
selectedKeysIterable<Key>-The currently selected keys (controlled)
defaultSelectedKeysIterable<Key>-The initial selected keys (uncontrolled)
onSelectionChange(keys: Selection) => void-Handler called when the selection changes
disabledKeysIterable<Key>-Keys of disabled items
onAction(key: Key) => void-Handler called when an item is activated
classNamestring-Additional CSS classes
childrenReactNode-Menu content

All Menu props are also supported.

PropTypeDefaultDescription
selectionMode"single" | "multiple"-Selection mode for items within this section
selectedKeysIterable<Key>-The currently selected keys (controlled)
defaultSelectedKeysIterable<Key>-The initial selected keys (uncontrolled)
onSelectionChange(keys: Selection) => void-Handler called when the selection changes
disabledKeysIterable<Key>-Keys of disabled items
classNamestring-Additional CSS classes
childrenReactNode-Section content

All MenuSection props are also supported.

PropTypeDefaultDescription
idKey-Unique identifier for the item
textValuestring-Text content of the item for typeahead
variant"default" | "danger""default"Visual variant of the item
classNamestring-Additional CSS classes
childrenReactNode | RenderFunction-Item content or render function

All MenuItem props are also supported.

PropTypeDefaultDescription
type"checkmark" | "dot""checkmark"Type of indicator to display
classNamestring-Additional CSS classes
childrenReactNode | RenderFunction-Custom indicator content or render function

When using a render function, these values are provided:

PropTypeDescription
isSelectedbooleanWhether the item is selected
isIndeterminatebooleanWhether the item is in an indeterminate state
PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReactNode-Custom indicator content
PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReactNode-Submenu trigger content

All SubmenuTrigger props are also supported.

Render Props

When using render functions with Dropdown.Item, these values are provided:

PropTypeDescription
isSelectedbooleanWhether the item is selected
isFocusedbooleanWhether the item is focused
isDisabledbooleanWhether the item is disabled
isPressedbooleanWhether 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.