HapplyUI

Components

Dropdown

A dropdown menu component with items, groups, sub-menus, error variants, and a composed shorthand for common patterns.


Installation

bunx @happlyui/cli@latest add dropdown

Dependencies

npm packages

  • @radix-ui/react-dropdown-menu
  • @remixicon/react

Registry dependencies

These are automatically installed when you add this component.

  • happly-ui-utils
  • polymorphic

Usage

import * as Dropdown from "@/components/ui/dropdown"

<Dropdown.Root>
  <Dropdown.Trigger asChild>
    <button>Open</button>
  </Dropdown.Trigger>
  <Dropdown.Content>
    <Dropdown.Item>Item</Dropdown.Item>
  </Dropdown.Content>
</Dropdown.Root>

Examples

Default

Basic dropdown with icon items.

<Dropdown.Root>
  <Dropdown.Trigger asChild>
    <Button.Root variant="neutral" mode="stroke">
      Open Dropdown
    </Button.Root>
  </Dropdown.Trigger>
  <Dropdown.Content align="start">
    <Dropdown.Group>
      <Dropdown.Item>
        <Dropdown.ItemIcon as={RiSettings2Line} />
        Settings
      </Dropdown.Item>
      <Dropdown.Item>
        <Dropdown.ItemIcon as={RiAddLine} />
        Add Item
      </Dropdown.Item>
      <Dropdown.Item>
        <Dropdown.ItemIcon as={RiLogoutBoxRLine} />
        Logout
      </Dropdown.Item>
    </Dropdown.Group>
  </Dropdown.Content>
</Dropdown.Root>

With Groups

Grouped items with labels and a divider between sections.

<Dropdown.Root>
  <Dropdown.Trigger asChild>
    <Button.Root variant="neutral" mode="stroke">
      Open Dropdown
    </Button.Root>
  </Dropdown.Trigger>
  <Dropdown.Content align="start">
    <Dropdown.Group>
      <Dropdown.Label>Account</Dropdown.Label>
      <Dropdown.Item>
        <Dropdown.ItemIcon as={RiPulseLine} />
        Activity
      </Dropdown.Item>
      <Dropdown.Item>
        <Dropdown.ItemIcon as={RiSettings2Line} />
        Settings
      </Dropdown.Item>
    </Dropdown.Group>
    <Divider.Root variant="line-spacing" />
    <Dropdown.Group>
      <Dropdown.Label>Actions</Dropdown.Label>
      <Dropdown.Item>
        <Dropdown.ItemIcon as={RiAddLine} />
        Add Account
      </Dropdown.Item>
      <Dropdown.Item variant="error">
        <Dropdown.ItemIcon as={RiLogoutBoxRLine} />
        Logout
      </Dropdown.Item>
    </Dropdown.Group>
  </Dropdown.Content>
</Dropdown.Root>

Dropdown with a user profile header, menu groups, and footer text.

<Dropdown.Root>
  <Dropdown.Trigger asChild>
    <Button.Root variant="neutral" mode="stroke">
      Open Dropdown
    </Button.Root>
  </Dropdown.Trigger>
  <Dropdown.Content align="start">
    <div className="flex items-center gap-3 p-2">
      <Avatar.Root size="40" />
      <div className="flex-1">
        <div className="text-label-sm text-text-strong-950">Wei Chen</div>
        <div className="text-paragraph-xs text-text-sub-600 mt-1">wei@alignui.com</div>
      </div>
      <Badge.Root variant="light" color="green" size="medium">PRO</Badge.Root>
    </div>
    <Divider.Root variant="line-spacing" />
    <Dropdown.Group>
      <Dropdown.Item>
        <Dropdown.ItemIcon as={RiPulseLine} />
        Activity
      </Dropdown.Item>
      <Dropdown.Item>
        <Dropdown.ItemIcon as={RiLayoutGridLine} />
        Integrations
      </Dropdown.Item>
      <Dropdown.Item>
        <Dropdown.ItemIcon as={RiSettings2Line} />
        Settings
      </Dropdown.Item>
    </Dropdown.Group>
    <Divider.Root variant="line-spacing" />
    <Dropdown.Group>
      <Dropdown.Item>
        <Dropdown.ItemIcon as={RiAddLine} />
        Add Account
      </Dropdown.Item>
      <Dropdown.Item>
        <Dropdown.ItemIcon as={RiLogoutBoxRLine} />
        Logout
      </Dropdown.Item>
    </Dropdown.Group>
    <div className="text-paragraph-sm text-text-soft-400 p-2">
      v.1.5.69 · Terms & Conditions
    </div>
  </Dropdown.Content>
</Dropdown.Root>

With Error Item

Dropdown with a destructive action styled in error red, separated from other items.

<Dropdown.Root>
  <Dropdown.Trigger asChild>
    <Button.Root variant="neutral" mode="stroke">Actions</Button.Root>
  </Dropdown.Trigger>
  <Dropdown.Content align="start">
    <Dropdown.Group>
      <Dropdown.Item>
        <Dropdown.ItemIcon as={RiPencilLine} />
        Edit
      </Dropdown.Item>
      <Dropdown.Item>
        <Dropdown.ItemIcon as={RiSettings2Line} />
        Settings
      </Dropdown.Item>
    </Dropdown.Group>
    <Divider.Root variant="line-spacing" />
    <Dropdown.Group>
      <Dropdown.Item variant="error">
        <Dropdown.ItemIcon as={RiDeleteBinLine} />
        Delete
      </Dropdown.Item>
    </Dropdown.Group>
  </Dropdown.Content>
</Dropdown.Root>

With Disabled Items

Dropdown with a disabled item that cannot be selected.

<Dropdown.Root>
  <Dropdown.Trigger asChild>
    <Button.Root variant="neutral" mode="stroke">
      Open Dropdown
    </Button.Root>
  </Dropdown.Trigger>
  <Dropdown.Content align="start">
    <Dropdown.Group>
      <Dropdown.Item>
        <Dropdown.ItemIcon as={RiPencilLine} />
        Edit
      </Dropdown.Item>
      <Dropdown.Item disabled>
        <Dropdown.ItemIcon as={RiRocketLine} />
        Publish (unavailable)
      </Dropdown.Item>
      <Dropdown.Item>
        <Dropdown.ItemIcon as={RiSettings2Line} />
        Settings
      </Dropdown.Item>
    </Dropdown.Group>
  </Dropdown.Content>
</Dropdown.Root>

Sub Menu

Dropdown with nested sub-menus for hierarchical navigation.

<Dropdown.Root>
  <Dropdown.Trigger asChild>
    <Button.Root variant="neutral" mode="stroke">Open</Button.Root>
  </Dropdown.Trigger>
  <Dropdown.Content align="start">
    <Dropdown.Group>
      <Dropdown.Item>
        <Dropdown.ItemIcon as={RiAddLine} />
        New post creation
      </Dropdown.Item>
      <Dropdown.MenuSub>
        <Dropdown.MenuSubTrigger>
          <Dropdown.ItemIcon as={RiFileList2Line} />
          Template Options
        </Dropdown.MenuSubTrigger>
        <Dropdown.MenuSubContent>
          <Dropdown.Item>
            <Dropdown.ItemIcon as={RiPencilLine} />
            Draft Post
          </Dropdown.Item>
          <Dropdown.Item>
            <Dropdown.ItemIcon as={RiRocketLine} />
            Publish Now
          </Dropdown.Item>
          <Dropdown.Item>
            <Dropdown.ItemIcon as={RiCalendar2Line} />
            Schedule Post
          </Dropdown.Item>
        </Dropdown.MenuSubContent>
      </Dropdown.MenuSub>
    </Dropdown.Group>
  </Dropdown.Content>
</Dropdown.Root>

Composed

The composed shorthand renders a full dropdown from a groups array. Separators are added between groups automatically.

<Dropdown.Composed
  groups={[
    {
      items: [
        { label: 'Activity', icon: RiPulseLine, onSelect: () => console.log('Activity') },
        { label: 'Integrations', icon: RiLayoutGridLine, onSelect: () => console.log('Integrations') },
        { label: 'Settings', icon: RiSettings2Line, onSelect: () => console.log('Settings') },
      ],
    },
    {
      items: [
        { label: 'Add Account', icon: RiAddLine },
        { label: 'Logout', icon: RiLogoutBoxRLine, variant: 'error' },
      ],
    },
  ]}
>
  <Button.Root variant="neutral" mode="stroke">
    Open Composed
  </Button.Root>
</Dropdown.Composed>

Composed dropdown with a user profile header and footer text.

<Dropdown.Composed
  groups={[
    {
      items: [
        { label: 'Activity', icon: RiPulseLine },
        { label: 'Integrations', icon: RiLayoutGridLine },
        { label: 'Settings', icon: RiSettings2Line },
      ],
    },
    {
      items: [
        { label: 'Add Account', icon: RiAddLine },
        { label: 'Logout', icon: RiLogoutBoxRLine, variant: 'error' },
      ],
    },
  ]}
  header={
    <>
      <div className="flex items-center gap-3 p-2">
        <Avatar.Root size="40" />
        <div className="flex-1">
          <div className="text-label-sm text-text-strong-950">Wei Chen</div>
          <div className="text-paragraph-xs text-text-sub-600 mt-1">wei@alignui.com</div>
        </div>
        <Badge.Root variant="light" color="green" size="medium">PRO</Badge.Root>
      </div>
      <Divider.Root variant="line-spacing" />
    </>
  }
  footer={
    <div className="text-paragraph-sm text-text-soft-400 p-2">
      v.1.5.69 · Terms & Conditions
    </div>
  }
>
  <Button.Root variant="neutral" mode="stroke">
    Profile Menu
  </Button.Root>
</Dropdown.Composed>

Composed with Labels

Composed dropdown with group labels for section headings.

<Dropdown.Composed
  groups={[
    {
      label: 'Account',
      items: [
        { label: 'Activity', icon: RiPulseLine },
        { label: 'Settings', icon: RiSettings2Line },
      ],
    },
    {
      label: 'Danger zone',
      items: [
        { label: 'Delete account', icon: RiDeleteBinLine, variant: 'error' },
      ],
    },
  ]}
>
  <Button.Root variant="neutral" mode="stroke">
    Account
  </Button.Root>
</Dropdown.Composed>

Composed dropdown with href items that render as anchor tags.

<Dropdown.Composed
  groups={[
    {
      items: [
        { label: 'Dashboard', icon: RiLayoutGridLine, href: '#dashboard' },
        { label: 'Settings', icon: RiSettings2Line, href: '#settings' },
        { label: 'Documentation', icon: RiGlobeLine, href: 'https://example.com' },
      ],
    },
  ]}
>
  <Button.Root variant="neutral" mode="stroke">
    Navigation
  </Button.Root>
</Dropdown.Composed>

API Reference

Dropdown.Root

The root dropdown container. Passes through Radix DropdownMenu.Root props.

PropTypeDefaultDescription
openboolean-Controlled open state
onOpenChange(open: boolean) => void-Callback when open state changes

Dropdown.Trigger

The element that opens the dropdown.

PropTypeDefaultDescription
asChildbooleanfalseRender as child element using Radix Slot

Dropdown.Content

The dropdown content panel with animations.

PropTypeDefaultDescription
side'top' | 'right' | 'bottom' | 'left''bottom'The preferred side to position the dropdown
align'start' | 'center' | 'end''center'The alignment relative to the trigger
sideOffsetnumber8Distance in pixels from the trigger

Dropdown.Item

A selectable dropdown menu item.

PropTypeDefaultDescription
variant'default' | 'error''default'Visual variant. Error renders text and icon in red.
insetbooleanfalseAdd left padding for alignment with icon items
disabledbooleanfalseDisables the item
onSelect() => void-Called when the item is selected

Dropdown.ItemIcon

Polymorphic icon displayed inside a dropdown item. Inherits error color from variant.

PropTypeDefaultDescription
asReact.ElementType-The icon component to render

Dropdown.Group

Groups related dropdown items.

PropTypeDefaultDescription

Dropdown.Label

An uppercase label for a group of dropdown items.

PropTypeDefaultDescription

Dropdown.MenuSub

Container for a sub-menu.

PropTypeDefaultDescription

Dropdown.MenuSubTrigger

The item that opens a sub-menu. Shows a right arrow indicator.

PropTypeDefaultDescription
insetbooleanfalseAdd left padding for alignment

Dropdown.MenuSubContent

The sub-menu content panel.

PropTypeDefaultDescription

Dropdown.Composed

A composed shorthand that renders a full dropdown from a groups array. Handles trigger, content, items, separators, icons, and variants automatically.

PropTypeDefaultDescription
childrenReact.ReactNode-The trigger element. Rendered via asChild.
groupsDropdownMenuGroup[]-Array of menu groups. Each group has an optional label and an items array. Each item has label, optional icon, variant, disabled, onSelect, and href.
align'start' | 'center' | 'end''start'Content alignment relative to trigger
side'top' | 'right' | 'bottom' | 'left'-Content side relative to trigger
sideOffsetnumber-Distance in pixels from the trigger
contentClassNamestring-Additional className for the content panel
headerReact.ReactNode-Header content rendered above menu items
footerReact.ReactNode-Footer content rendered below menu items
openboolean-Controlled open state
onOpenChange(open: boolean) => void-Callback when open state changes

Previous
Drawer