Dropdown
NewOverview
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.
<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.
<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.
<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.
<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.
<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.
<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
<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.
<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.
<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
mix dizzy.add dropdown
This example also uses a modal. Add it with mix dizzy.add modal.
Component API
Dropdown.dropdown
Attributes
| Name | Type | Default | 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
| :flip | nil, :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
| Name | Required |
|---|---|
item | Required |
trigger_element | Optional |
Item slot attributes
| Name | Type | Default | 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.