Dropdown

free

Dropdown keeps actions and option lists available from a compact trigger.

@floating-ui/react
Preview
Dark
import Avatar from '@/components/ui/Avatar';
import Dropdown from '@/components/ui/Dropdown';
import Switcher from '@/components/ui/Switcher';
import {
    PiCaretUpDown,
    PiGear,
    PiMoon,
    PiPulse,
    PiSignOut,
    PiUserCircle,
} from 'react-icons/pi'

export default function UsageDemo() {
  return (
    <div className="flex justify-center">
      <Dropdown
        menuClass="min-w-56"
        renderTitle={
          <div className="flex cursor-pointer items-center gap-2 rounded-control p-2 transition-colors hover:bg-accent">
            <Avatar size="sm" className="bg-palette-purple text-white">
              JB
            </Avatar>
            <div className="text-left">
              <div className="text-sm font-semibold text-foreground">
                Jordan Blake
              </div>
              <div className="text-xs text-muted-foreground">
                jordan@example.com
              </div>
            </div>
            <PiCaretUpDown className="text-base text-muted-foreground" />
          </div>
        }
      >
        <Dropdown.Item eventKey="profile" className="flex items-center gap-2">
          <PiUserCircle className="text-base text-muted-foreground" />
          Profile
        </Dropdown.Item>
        <Dropdown.Item eventKey="settings" className="flex items-center gap-2">
          <PiGear className="text-base text-muted-foreground" />
          Account Settings
        </Dropdown.Item>
        <Dropdown.Item eventKey="activity" className="flex items-center gap-2">
          <PiPulse className="text-base text-muted-foreground" />
          Activity Log
        </Dropdown.Item>
        <Dropdown.Item variant="divider" />
        <Dropdown.Item
          variant="custom"
          closeOnClick={false}
          className="flex w-full cursor-pointer items-center justify-between gap-2 rounded-control-sm px-3 py-2 font-medium text-popover-foreground hover:bg-accent hover:text-accent-foreground"
        >
          <span className="flex items-center gap-2">
            <PiMoon className="text-base text-muted-foreground" />
            Dark Mode
          </span>
          <Switcher aria-label="Dark mode" defaultChecked />
        </Dropdown.Item>
        <Dropdown.Item variant="divider" />
        <Dropdown.Item
          variant="custom"
          eventKey="sign-out"
          className="flex cursor-pointer items-center gap-2 rounded-control-sm px-3 py-2 font-medium text-destructive"
        >
          <PiSignOut className="text-base" />
          Sign Out
        </Dropdown.Item>
      </Dropdown>
    </div>
  );
}

Installation

Add this component with the NateUI CLI.

npx nateui@latest add Dropdown

Examples

Basic

Preview
Dark
import Dropdown from '@/components/ui/Dropdown';

export default function BasicDemo() {
  return (
    <div className="flex justify-center">
      <Dropdown title="Options">
        <Dropdown.Item eventKey="edit">Edit</Dropdown.Item>
        <Dropdown.Item eventKey="duplicate">Duplicate</Dropdown.Item>
        <Dropdown.Item eventKey="archive">Archive</Dropdown.Item>
      </Dropdown>
    </div>
  );
}

Custom Toggle

Preview
Dark
import { PiArchive, PiCopy, PiPencilSimple } from 'react-icons/pi'
import Button from '@/components/ui/Button';
import Dropdown from '@/components/ui/Dropdown';

export default function CustomToggleDemo() {
  return (
    <div className="flex justify-center">
      <Dropdown renderTitle={<Button variant="solid">Actions</Button>}>
        <Dropdown.Item eventKey="edit" className="flex items-center gap-2">
          <PiPencilSimple className="text-base text-muted-foreground" />
          Edit
        </Dropdown.Item>
        <Dropdown.Item eventKey="duplicate" className="flex items-center gap-2">
          <PiCopy className="text-base text-muted-foreground" />
          Duplicate
        </Dropdown.Item>
        <Dropdown.Item eventKey="archive" className="flex items-center gap-2">
          <PiArchive className="text-base text-muted-foreground" />
          Archive
        </Dropdown.Item>
      </Dropdown>
    </div>
  );
}

Trigger

Preview
Dark
import { PiArchive, PiCopy, PiPencilSimple } from 'react-icons/pi'
import Dropdown from '@/components/ui/Dropdown';

export default function TriggerDemo() {
  return (
    <div className="flex flex-wrap justify-center gap-2">
      <Dropdown title="Click">
        <Dropdown.Item eventKey="edit" className="flex items-center gap-2">
          <PiPencilSimple className="text-base text-muted-foreground" />
          Edit
        </Dropdown.Item>
        <Dropdown.Item eventKey="duplicate" className="flex items-center gap-2">
          <PiCopy className="text-base text-muted-foreground" />
          Duplicate
        </Dropdown.Item>
        <Dropdown.Item eventKey="archive" className="flex items-center gap-2">
          <PiArchive className="text-base text-muted-foreground" />
          Archive
        </Dropdown.Item>
      </Dropdown>

      <Dropdown title="Hover" trigger="hover">
        <Dropdown.Item eventKey="edit" className="flex items-center gap-2">
          <PiPencilSimple className="text-base text-muted-foreground" />
          Edit
        </Dropdown.Item>
        <Dropdown.Item eventKey="duplicate" className="flex items-center gap-2">
          <PiCopy className="text-base text-muted-foreground" />
          Duplicate
        </Dropdown.Item>
        <Dropdown.Item eventKey="archive" className="flex items-center gap-2">
          <PiArchive className="text-base text-muted-foreground" />
          Archive
        </Dropdown.Item>
      </Dropdown>

      <Dropdown title="Right Click" trigger="rightClick">
        <Dropdown.Item eventKey="edit" className="flex items-center gap-2">
          <PiPencilSimple className="text-base text-muted-foreground" />
          Edit
        </Dropdown.Item>
        <Dropdown.Item eventKey="duplicate" className="flex items-center gap-2">
          <PiCopy className="text-base text-muted-foreground" />
          Duplicate
        </Dropdown.Item>
        <Dropdown.Item eventKey="archive" className="flex items-center gap-2">
          <PiArchive className="text-base text-muted-foreground" />
          Archive
        </Dropdown.Item>
      </Dropdown>
    </div>
  );
}
Preview
Dark
import {
    PiArchive,
    PiEnvelopeSimple,
    PiFolderOpen,
    PiFolderPlus,
    PiPencilSimple,
    PiTrash,
    PiTray,
} from 'react-icons/pi'
import Dropdown from '@/components/ui/Dropdown';

export default function SubmenuDemo() {
  return (
    <div className="flex justify-center">
      <Dropdown title="Options">
        <Dropdown.Item eventKey="edit" className="flex items-center gap-2">
          <PiPencilSimple className="text-base text-muted-foreground" />
          Edit
        </Dropdown.Item>
        <Dropdown.Menu
          renderTitle={
            <span className="flex items-center gap-2">
              <PiFolderPlus className="text-base text-muted-foreground" />
              Move to
            </span>
          }
        >
          <Dropdown.Item eventKey="move-inbox" className="flex items-center gap-2">
            <PiTray className="text-base text-muted-foreground" />
            Inbox
          </Dropdown.Item>
          <Dropdown.Item eventKey="move-archive" className="flex items-center gap-2">
            <PiArchive className="text-base text-muted-foreground" />
            Archive
          </Dropdown.Item>
          <Dropdown.Menu
            renderTitle={
              <span className="flex items-center gap-2">
                <PiFolderOpen className="text-base text-muted-foreground" />
                More
              </span>
            }
          >
            <Dropdown.Item eventKey="move-trash" className="flex items-center gap-2">
              <PiTrash className="text-base text-muted-foreground" />
              Trash
            </Dropdown.Item>
            <Dropdown.Item eventKey="move-spam" className="flex items-center gap-2">
              <PiEnvelopeSimple className="text-base text-muted-foreground" />
              Spam
            </Dropdown.Item>
          </Dropdown.Menu>
        </Dropdown.Menu>
        <Dropdown.Item eventKey="delete" className="flex items-center gap-2">
          <PiTrash className="text-base text-muted-foreground" />
          Delete
        </Dropdown.Item>
      </Dropdown>
    </div>
  );
}

Placement

Preview
Dark
import { PiArchive, PiCopy, PiPencilSimple } from 'react-icons/pi'
import Dropdown from '@/components/ui/Dropdown';

const DropdownItems = () => (
  <>
    <Dropdown.Item eventKey="edit" className="flex items-center gap-2">
      <PiPencilSimple className="text-base text-muted-foreground" />
      Edit
    </Dropdown.Item>
    <Dropdown.Item eventKey="duplicate" className="flex items-center gap-2">
      <PiCopy className="text-base text-muted-foreground" />
      Duplicate
    </Dropdown.Item>
    <Dropdown.Item eventKey="archive" className="flex items-center gap-2">
      <PiArchive className="text-base text-muted-foreground" />
      Archive
    </Dropdown.Item>
  </>
);

export default function PlacementDemo() {
  return (
    <div className="grid grid-cols-2 gap-4 sm:grid-cols-3">
      <Dropdown placement="top-start" title="Top start">
        <DropdownItems />
      </Dropdown>
      <Dropdown placement="top" title="Top">
        <DropdownItems />
      </Dropdown>
      <Dropdown placement="top-end" title="Top end">
        <DropdownItems />
      </Dropdown>
      <Dropdown placement="bottom-start" title="Bottom start">
        <DropdownItems />
      </Dropdown>
      <Dropdown placement="bottom" title="Bottom">
        <DropdownItems />
      </Dropdown>
      <Dropdown placement="bottom-end" title="Bottom end">
        <DropdownItems />
      </Dropdown>
      <Dropdown placement="right-start" title="Right start">
        <DropdownItems />
      </Dropdown>
      <Dropdown placement="right" title="Right">
        <DropdownItems />
      </Dropdown>
      <Dropdown placement="right-end" title="Right end">
        <DropdownItems />
      </Dropdown>
      <Dropdown placement="left-start" title="Left start">
        <DropdownItems />
      </Dropdown>
      <Dropdown placement="left" title="Left">
        <DropdownItems />
      </Dropdown>
      <Dropdown placement="left-end" title="Left end">
        <DropdownItems />
      </Dropdown>
    </div>
  );
}

Active Item

Preview
Dark
import { PiSortAscending, PiSortDescending, PiTextAa } from 'react-icons/pi'
import Dropdown from '@/components/ui/Dropdown';

export default function ActiveItemDemo() {
  return (
    <div className="flex justify-center">
      <Dropdown title="Sort by" activeKey="newest">
        <Dropdown.Item eventKey="newest" className="flex items-center gap-2">
          <PiSortDescending className="text-base text-muted-foreground" />
          Newest first
        </Dropdown.Item>
        <Dropdown.Item eventKey="oldest" className="flex items-center gap-2">
          <PiSortAscending className="text-base text-muted-foreground" />
          Oldest first
        </Dropdown.Item>
        <Dropdown.Item eventKey="az" className="flex items-center gap-2">
          <PiTextAa className="text-base text-muted-foreground" />
          A&ndash;Z
        </Dropdown.Item>
      </Dropdown>
    </div>
  );
}

Disabled

Preview
Dark
import { PiArchive, PiCopy, PiPencilSimple } from 'react-icons/pi'
import Dropdown from '@/components/ui/Dropdown';

export default function DisabledDemo() {
  return (
    <div className="flex flex-wrap justify-center gap-2">
      <Dropdown disabled title="Options">
        <Dropdown.Item eventKey="edit" className="flex items-center gap-2">
          <PiPencilSimple className="text-base text-muted-foreground" />
          Edit
        </Dropdown.Item>
        <Dropdown.Item eventKey="duplicate" className="flex items-center gap-2">
          <PiCopy className="text-base text-muted-foreground" />
          Duplicate
        </Dropdown.Item>
        <Dropdown.Item eventKey="archive" className="flex items-center gap-2">
          <PiArchive className="text-base text-muted-foreground" />
          Archive
        </Dropdown.Item>
      </Dropdown>

      <Dropdown title="Options">
        <Dropdown.Item eventKey="edit" className="flex items-center gap-2">
          <PiPencilSimple className="text-base text-muted-foreground" />
          Edit
        </Dropdown.Item>
        <Dropdown.Item
          eventKey="duplicate"
          disabled
          className="flex items-center gap-2"
        >
          <PiCopy className="text-base text-muted-foreground" />
          Duplicate
        </Dropdown.Item>
        <Dropdown.Item eventKey="archive" className="flex items-center gap-2">
          <PiArchive className="text-base text-muted-foreground" />
          Archive
        </Dropdown.Item>
      </Dropdown>
    </div>
  );
}

Item Variants

Preview
Dark
import { ReactNode } from 'react';
import {
    PiBuilding,
    PiCreditCard,
    PiGear,
    PiHeadphones,
    PiSignOut,
    PiUser,
    PiUsers,
} from 'react-icons/pi'
import Dropdown from '@/components/ui/Dropdown';
import Kbd from '@/components/ui/Kbd';

const ItemContent = ({
  icon,
  suffix,
  children,
}: {
  icon: ReactNode;
  suffix?: string;
  children: ReactNode;
}) => (
  <div className="flex w-full items-center justify-between gap-2">
    <div className="flex items-center gap-2">
      {icon}
      {children}
    </div>
    {suffix && <Kbd>{suffix}</Kbd>}
  </div>
);

export default function ItemVariantsDemo() {
  return (
    <div className="flex justify-center">
      <Dropdown title="Account">
        <Dropdown.Item
          variant="header"
          className="px-2 py-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground"
        >
          My Account
        </Dropdown.Item>
        <Dropdown.Item variant="divider" />
        <Dropdown.Item eventKey="profile">
          <ItemContent
            icon={<PiUser className="text-base text-muted-foreground" />}
            suffix="⇧⌘P"
          >
            Profile
          </ItemContent>
        </Dropdown.Item>
        <Dropdown.Item eventKey="settings">
          <ItemContent
            icon={<PiGear className="text-base text-muted-foreground" />}
            suffix="⌘,"
          >
            Settings
          </ItemContent>
        </Dropdown.Item>
        <Dropdown.Item eventKey="billing">
          <ItemContent
            icon={<PiCreditCard className="text-base text-muted-foreground" />}
            suffix="⌘B"
          >
            Billing
          </ItemContent>
        </Dropdown.Item>
        <Dropdown.Item eventKey="support">
          <ItemContent
            icon={<PiHeadphones className="text-base text-muted-foreground" />}
            suffix="⌘/"
          >
            Support
          </ItemContent>
        </Dropdown.Item>
        <Dropdown.Item variant="divider" />
        <Dropdown.Item eventKey="team">
          <ItemContent icon={<PiUsers className="text-base text-muted-foreground" />}>
            Team
          </ItemContent>
        </Dropdown.Item>
        <Dropdown.Item eventKey="organization">
          <ItemContent
            icon={<PiBuilding className="text-base text-muted-foreground" />}
          >
            Organization
          </ItemContent>
        </Dropdown.Item>
        <Dropdown.Item variant="divider" />
        <Dropdown.Item
          variant="custom"
          eventKey="logout"
          className="flex cursor-pointer items-center gap-2 rounded-control-sm px-3 py-2 font-medium text-destructive hover:bg-destructive-soft"
        >
          <PiSignOut className="text-base" />
          Logout
        </Dropdown.Item>
      </Dropdown>
    </div>
  );
}

Context Menu

Preview
Dark
import { PiCopy, PiPencilSimple, PiTrash } from 'react-icons/pi'
import Dropdown from '@/components/ui/Dropdown';

export default function ContextMenuDemo() {
  return (
    <Dropdown.ContextMenu
      areaClass="flex h-36 min-w-52 items-center justify-center rounded-card border border-dashed border-border bg-muted text-muted-foreground"
      areaContent={<span className="font-medium">Right click here</span>}
    >
      <Dropdown.Item eventKey="edit" className="flex items-center gap-2">
        <PiPencilSimple className="text-base text-muted-foreground" />
        Edit
      </Dropdown.Item>
      <Dropdown.Item eventKey="duplicate" className="flex items-center gap-2">
        <PiCopy className="text-base text-muted-foreground" />
        Duplicate
      </Dropdown.Item>
      <Dropdown.Item eventKey="delete" className="flex items-center gap-2">
        <PiTrash className="text-base text-muted-foreground" />
        Delete
      </Dropdown.Item>
    </Dropdown.ContextMenu>
  );
}
Preview
Dark
import { PiArrowRight } from 'react-icons/pi'
import Dropdown from '@/components/ui/Dropdown';

export default function LinkDemo() {
  return (
    <div className="flex justify-center">
      <Dropdown title="Components">
        <Dropdown.Item
          asElement="a"
          href="/components/ui/checkbox"
          className="flex items-center gap-2"
        >
          <PiArrowRight className="text-base text-muted-foreground" />
          Checkbox
        </Dropdown.Item>
        <Dropdown.Item
          asElement="a"
          href="/components/ui/button"
          className="flex items-center gap-2"
        >
          <PiArrowRight className="text-base text-muted-foreground" />
          Button
        </Dropdown.Item>
        <Dropdown.Item
          asElement="a"
          href="/components/ui/alert"
          className="flex items-center gap-2"
        >
          <PiArrowRight className="text-base text-muted-foreground" />
          Alert
        </Dropdown.Item>
        <Dropdown.Item
          asElement="a"
          href="/components/ui/dialog"
          className="flex items-center gap-2"
        >
          <PiArrowRight className="text-base text-muted-foreground" />
          Dialog
        </Dropdown.Item>
      </Dropdown>
    </div>
  );
}

API

Dropdown is a compound component: the root trigger, Dropdown.Item, Dropdown.Menu for submenus, and Dropdown.ContextMenu for right-click menus.

PropDescriptionTypeDefault
activeKeyMatches a Dropdown.Item's eventKey to mark it active.string-
disabledPrevents the dropdown from opening.boolean-
menuClassClass names applied to the menu surface.string-
onOpenCalled when the open state changes.DropdownOpenChangeHandler-
onSelectCalled when a Dropdown.Item inside is selected.DropdownSelectHandler-
placementSide and alignment the menu opens from, relative to the trigger.DropdownPlacement'bottom-start'
renderTitleCustom trigger content, replacing the default toggle.ReactNode-
titleLabel rendered in the default toggle.string | ReactNode-
toggleClassNameClass names applied to the trigger element.string-
triggerHow the menu opens: on click, hover, or right-click.'click' | 'hover' | 'rightClick''click'
...nativePropsNative props applied to the trigger element.ComponentProps<'div'>-
PropDescriptionTypeDefault
activeMarks the item as active, independent of activeKey matching.boolean-
asElementElement or component the item renders as.ElementType'li'
closeOnClickCloses the dropdown after this item is selected.booleantrue
disabledPrevents interaction with this item.boolean-
eventKeyValue passed to onSelect and matched against activeKey.string-
menuItemHeightFixed height of the item, in pixels.number36
onClickCalled when the item is clicked.(e: SyntheticEvent) => void-
onFocusCalled when the item receives focus.(e: SyntheticEvent) => void-
onSelectCalled with the item's eventKey when selected.DropdownSelectHandler-
variantVisual role of the item.DropdownItemVariant'default'
...nativePropsNative props applied to the rendered element.ComponentProps<'li'>-

Renders a nested submenu. Used as a child of Dropdown or another Dropdown.Menu.

PropDescriptionTypeDefault
eventKeyValue matched against the parent's activeKey to highlight this submenu.string-
menuClassClass names applied to the submenu surface.string-
onOpenCalled when the submenu's open state changes.DropdownOpenChangeHandler-
placementSide the submenu opens from, relative to its parent item.DropdownPlacement'right-start'
renderTitleCustom content replacing the default submenu trigger row.ReactNode-
titleLabel rendered as the submenu's trigger row.string | ReactNode-

Renders its areaContent inside a region that opens the menu on right-click, instead of a click-to-open toggle.

PropDescriptionTypeDefault
activeKeyMatches a Dropdown.Item's eventKey to mark it active.string-
areaClassClass names applied to the right-click area.string-
areaContentContent rendered inside the right-click area. Required.ReactNode-
areaRefRef forwarded to the right-click area element.Ref<HTMLDivElement>-
childrenDropdown.Item / Dropdown.Menu elements rendered as the menu content.ReactNode-
disabledPrevents the menu from opening.boolean-
menuClassClass names applied to the menu surface.string-
onOpenCalled when the open state changes — right-click (or openAt/handleDropdownOpen) to open, and Escape, outside click, or handleDropdownClose to close.DropdownOpenChangeHandler-
placementSide and alignment the menu opens from, relative to the cursor.DropdownPlacement'bottom-start'
refImperative handle for opening and closing the menu programmatically.Ref<DropdownContextMenuRef>-

Types

type DropdownPlacement =
  | 'top'
  | 'top-start'
  | 'top-end'
  | 'right'
  | 'right-start'
  | 'right-end'
  | 'bottom'
  | 'bottom-start'
  | 'bottom-end'
  | 'left'
  | 'left-start'
  | 'left-end';
type DropdownItemVariant = 'default' | 'header' | 'divider' | 'custom';
type DropdownOpenChangeHandler = (open: boolean) => void;
type DropdownSelectHandler = (eventKey: string, e: SyntheticEvent) => void;
type DropdownContextMenuRef = {
  openAt: (x: number, y: number) => void;
  handleDropdownOpen: () => void;
  handleDropdownClose: () => void;
};