Dropdown

New

Overview

An action menu with keyboard navigation, icons, grouped items, descriptions, and shortcut hints.

Use this menu for actions. Use a popover for forms or controls that stay open while editing. Arrow keys move between items, including disabled actions; Enter selects an available action, and Escape closes the menu and returns focus to its trigger.

Dizzy / Component library / Prototype

HEEx
<div class="center-example grid justify-items-center gap-5">
  <Dropdown.dropdown id="example-menu" label="Project actions">
    <:item on_click={JS.push("demo-action", value: %{action: "Project link: /examples/settings"})}>
      View project link
    </:item>
    <:item on_click={Modal.show("menu-project-modal")}>Project details</:item>
    <:item disabled>Transfer ownership</:item>
    <:item
      destructive
      separator
      on_click={JS.push("demo-action", value: %{action: "Project archived in this demo"})}
    >
      Archive project
    </:item>
  </Dropdown.dropdown>
  <Modal.modal id="menu-project-modal">
    <:title>Project details</:title>
    <:description>Review the project name and current status.</:description>
    <p class="muted text-[#52525b] dark:text-[#a1a3ac] text-xs">
      Dizzy / Component library / Prototype
    </p>
  </Modal.modal>
</div>

Icons and shortcut hints

Icons align with labels and shortcut hints sit at the trailing edge. Hints are visual only; your application must register any keyboard bindings.

HEEx
<Dropdown.dropdown id="document-actions" label="Document actions">
  <:item
    icon="hero-pencil-square"
    shortcut="โŒ˜E"
    on_click={JS.push("demo-action", value: %{action: "Rename document selected"})}
  >
    Rename
  </:item>
  <:item
    icon="hero-document-duplicate"
    shortcut="โŒ˜D"
    on_click={JS.push("demo-action", value: %{action: "Duplicate document selected"})}
  >
    Duplicate
  </:item>
  <:item
    icon="hero-arrow-down-tray"
    shortcut="โ‡งโŒ˜S"
    on_click={JS.push("demo-action", value: %{action: "Download document selected"})}
  >
    Download
  </:item>
  <:item
    icon="hero-archive-box"
    separator
    on_click={JS.push("demo-action", value: %{action: "Document moved to archive"})}
  >
    Move to archive
  </:item>
</Dropdown.dropdown>

Sections and dividers

Start a section with an item heading and separate related actions with a divider. Arrow keys and typeahead move between actions, skipping these labels and dividers.

HEEx
<Dropdown.dropdown id="workspace-actions" label="Workspace actions">
  <:item
    heading="Workspace"
    icon="hero-users"
    on_click={JS.push("demo-action", value: %{action: "Invite a teammate selected"})}
  >
    Invite a teammate
  </:item>
  <:item
    icon="hero-link"
    on_click={JS.push("demo-action", value: %{action: "Share workspace selected"})}
  >
    Share workspace
  </:item>
  <:item
    heading="Preferences"
    separator
    icon="hero-bell"
    on_click={JS.push("demo-action", value: %{action: "Notification preferences selected"})}
  >
    Notifications
  </:item>
  <:item
    icon="hero-adjustments-horizontal"
    on_click={JS.push("demo-action", value: %{action: "Workspace preferences selected"})}
  >
    Preferences
  </:item>
  <:item
    separator
    icon="hero-arrow-right-start-on-rectangle"
    on_click={JS.push("demo-action", value: %{action: "Leave workspace selected"})}
  >
    Leave workspace
  </:item>
</Dropdown.dropdown>

Item descriptions

Add supporting text when a label needs context. Use menu_class to give longer descriptions more room.

HEEx
<Dropdown.dropdown id="publish-actions" label="Publish update" menu_class="w-72!">
  <:item
    icon="hero-document-text"
    description="Keep this update visible only to you."
    on_click={JS.push("demo-action", value: %{action: "Update saved as a draft"})}
  >
    Save as draft
  </:item>
  <:item
    icon="hero-users"
    description="Share with everyone in your workspace."
    on_click={JS.push("demo-action", value: %{action: "Update shared with the team"})}
  >
    Share with team
  </:item>
  <:item
    icon="hero-globe-alt"
    description="Make this update available to anyone."
    on_click={JS.push("demo-action", value: %{action: "Public publishing selected"})}
  >
    Publish publicly
  </:item>
</Dropdown.dropdown>

Disabled and destructive actions

Disabled actions remain discoverable with arrow keys but cannot run. Separate destructive actions and explain unavailable actions with a description. This example does not delete any data.

HEEx
<Dropdown.dropdown id="manage-document" label="Manage document">
  <:item
    icon="hero-pencil-square"
    on_click={JS.push("demo-action", value: %{action: "Edit document selected"})}
  >
    Edit document
  </:item>
  <:item icon="hero-lock-closed" disabled description="Only the owner can transfer this document.">
    Transfer ownership
  </:item>
  <:item
    icon="hero-trash"
    separator
    destructive
    on_click={
      JS.push("demo-action", value: %{action: "Delete requested โ€” no document was removed"})
    }
  >
    Delete document
  </:item>
</Dropdown.dropdown>

Custom trigger

Use Button with as_dropdown_trigger for a styled trigger. Its content remains non-interactive inside the menu trigger.

HEEx
<div class="center-example grid justify-items-center gap-5">
  <Dropdown.dropdown id="custom-trigger-menu">
    <:trigger_element>
      <Button.button as_dropdown_trigger kind={:secondary}>More actions</Button.button>
    </:trigger_element>
    <:item on_click={JS.push("demo-action", value: %{action: "Custom trigger: Edit project"})}>
      Edit project
    </:item>
    <:item on_click={
      JS.push("demo-action", value: %{action: "Custom trigger: Duplicate project"})
    }>
      Duplicate project
    </:item>
  </Dropdown.dropdown>
</div>

Row actions

Give an icon-only trigger a label that identifies its row. The menu aligns to the trailing edge by default.

Autumn release notes

Edited a few minutes ago

HEEx
<div class="mx-auto flex w-full max-w-sm items-center justify-between gap-4 rounded-xl border border-border p-4">
  <div class="min-w-0">
    <p class="truncate text-sm font-medium">Autumn release notes</p>
    <p class="mt-0.5 text-xs text-muted-foreground">Edited a few minutes ago</p>
  </div>
  <Dropdown.dropdown
    id="release-row-actions"
    trigger_aria_label="Actions for Autumn release notes"
  >
    <:trigger_element>
      <span class="inline-flex size-8 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground">
        <Icon.icon name="hero-ellipsis-horizontal" class="size-5" aria-hidden="true" />
      </span>
    </:trigger_element>
    <:item
      icon="hero-eye"
      on_click={JS.push("demo-action", value: %{action: "Release notes preview selected"})}
    >
      Preview notes
    </:item>
    <:item
      icon="hero-pencil-square"
      on_click={JS.push("demo-action", value: %{action: "Edit release notes selected"})}
    >
      Edit notes
    </:item>
    <:item
      icon="hero-trash"
      separator
      destructive
      on_click={JS.push("demo-action", value: %{action: "Release notes deletion requested"})}
    >
      Delete notes
    </:item>
  </Dropdown.dropdown>
</div>

Placement

Choose a side and alignment. Floating UI flips and shifts the panel when space is limited.

HEEx
<div class="flex flex-wrap justify-center gap-3">
  <Dropdown.dropdown
    :for={
      {label, side} <- [
        {"Above", :top_start},
        {"To the right", :right_start},
        {"Below", :bottom_end}
      ]
    }
    id={"placed-menu-#{side}"}
    label={label}
    side={side}
  >
    <:item icon="hero-eye" on_click={JS.push("demo-action", value: %{action: "Preview selected"})}>
      Preview
    </:item>
    <:item
      icon="hero-arrow-up-right"
      on_click={JS.push("demo-action", value: %{action: "Open in workspace selected"})}
    >
      Open in workspace
    </:item>
  </Dropdown.dropdown>
</div>

Scrollable menus

Long menus scroll within a capped surface. Arrow-key navigation keeps the focused action in view.

HEEx
<Dropdown.dropdown id="recent-documents" label="Recent documents">
  <:item
    :for={
      title <- [
        "Release notes",
        "Design review",
        "Onboarding checklist",
        "API reference",
        "Meeting notes",
        "Research summary",
        "Roadmap",
        "Brand guidelines",
        "Support handbook",
        "Launch plan",
        "Accessibility review",
        "Project archive"
      ]
    }
    icon="hero-document-text"
    on_click={JS.push("demo-action", value: %{action: "Opened #{title}"})}
  >
    {title}
  </:item>
</Dropdown.dropdown>

Add this module

terminal
mix dizzy.add dropdown

This example also uses a modal. Add it with mix dizzy.add modal.

Component API

Dropdown.dropdown

Attributes

Attributes for Dropdown.dropdown
NameTypeDefault Values
arrow :boolean falseโ€”
class :any nilโ€”
hide :boolean falseโ€”
id :string Requiredโ€”
label :string "Options"โ€”
menu_class :any nilโ€”
offset :integer 8โ€”
placement_strategy :atom :flipnil, :flip, :auto_placement
shift :boolean trueโ€”
side :atom :bottom_end:top, :top_start, :top_end, :bottom, :bottom_start, :bottom_end, :left, :left_start, :left_end, :right, :right_start, :right_end
strategy :atom :fixed:absolute, :fixed
trigger_aria_label :string nilโ€”

Slots

Slots for Dropdown.dropdown
NameRequired
item Required
trigger_element Optional

Item slot attributes

Attributes for the item slot in Dropdown.dropdown
NameTypeDefault Values
disabled :boolean nilโ€”
destructive :boolean nilโ€”
on_click :any nilโ€”
icon :string nilโ€”
description :string nilโ€”
shortcut :string nilโ€”
heading :string nilโ€”
separator :boolean nilโ€”

JavaScript hook

DizzyDropdown

Uses Floating UI to anchor the menu, flip it at edges, and update its position during scrolling and resizing. Coordinates keyboard navigation, typeahead, disabled items, outside-click dismissal, and focus restoration. Phoenix JS commands update visibility and expanded state; menu state stays in the browser.

mix dizzy.add includes the hook and performs any required asset setup automatically.

Explore Dizzy