Usage
<%= render "components/dropdown_menu" do %>
<%= render "components/dropdown_menu/trigger" do %>Open Menu<% end %>
<%= render "components/dropdown_menu/content" do %>
<%= render "components/dropdown_menu/item", href: "#" do %>Profile<% end %>
<%= render "components/dropdown_menu/item", href: "#" do %>Settings<% end %>
<%= render "components/dropdown_menu/separator" %>
<%= render "components/dropdown_menu/item", href: "#" do %>Logout<% end %>
<% end %>
<% end %>
Examples
With Icons
<%= render "components/dropdown_menu/item", href: "#" do %>
<%= icon_for :user, class: "size-4" %>
Profile
<% end %>
<%= render "components/dropdown_menu/item", href: "#", variant: :destructive do %>
<%= icon_for :log_out, class: "size-4" %>
Logout
<% end %>
With Shortcuts
<%= render "components/dropdown_menu/item", href: "#" do %>
Undo
<%= render "components/dropdown_menu/shortcut" do %>⌘Z<% end %>
<% end %>
Icon Trigger
The default trigger renders its own chevron, which rotates 180° while the menu is open. Reach for as_child when you need different content — an icon-only button, an sr-only label — not merely to get an affordance. Note that as_child hands you the whole button: data-dropdown-menu-target="trigger", data-action="dropdown-menu#toggle", aria-haspopup and aria-expanded are all yours to write. The controller updates aria-expanded at runtime, but only if the attribute is there to begin with.
<%= render "components/dropdown_menu/trigger", as_child: true do %>
<button type="button"
data-component="button"
data-variant="ghost"
data-size="icon"
data-dropdown-menu-target="trigger"
data-action="dropdown-menu#toggle"
aria-haspopup="menu"
aria-expanded="false">
<%= icon_for :more_horizontal, class: "size-4" %>
</button>
<% end %>
Placement
Since 0.7.1 the menu measures itself when it opens and flips above the trigger if it would otherwise open past the bottom of the viewport. It flips only when the space above genuinely fits — flipping into a gap that is also too small trades one clipped menu for another — and it re-measures from the placement you asked for each time, so a menu that flipped in a short window returns to its default once the window grows.
side: is therefore an initial preference rather than a fixed position. The controller writes the resolved side to data-side on the content element, which is what the CSS positions against. Only the block axis flips; :left and :right are left alone.
The menu button behaves the same way. If you carry your own collision or flip controller, you can delete it — bin/rails maquina:doctor reports it as app-level-dropdown-flip.
API Reference
Dropdown Menu
| Parameter |
Type |
Default |
Description |
| css_classes |
String |
"" |
Additional CSS classes |
| html_options |
Hash |
{} |
Additional HTML attributes |
Dropdown Menu Trigger
| Parameter |
Type |
Default |
Description |
| variant |
Symbol |
:outline |
Button variant when as_child is false |
| size |
Symbol |
:default |
Button size when as_child is false |
| as_child |
Boolean |
false |
Use custom trigger markup |
| css_classes |
String |
"" |
Additional CSS classes |
| html_options |
Hash |
{} |
Additional HTML attributes |
Dropdown Menu Content
| Parameter |
Type |
Default |
Description |
| align |
Symbol |
:start |
:start, :center, :end |
| side |
Symbol |
:bottom |
:top, :bottom, :left, :right — a preference; see Placement below |
| width |
Symbol |
:default |
:default, :sm, :md, :lg |
| css_classes |
String |
"" |
Additional CSS classes |
| html_options |
Hash |
{} |
Additional HTML attributes |
Dropdown Menu Item
| Parameter |
Type |
Default |
Description |
| href |
String |
nil |
URL, renders link if provided |
| method |
Symbol |
nil |
HTTP method (:delete, :post, etc.) |
| variant |
Symbol |
:default |
:default or :destructive |
| disabled |
Boolean |
false |
Whether disabled |
| css_classes |
String |
"" |
Additional CSS classes |
| html_options |
Hash |
{} |
Additional HTML attributes |
Dropdown Menu Label
| Parameter |
Type |
Default |
Description |
| text |
String |
nil |
Label text |
| content |
String |
nil |
Captured HTML via capture, or use block |
| inset |
Boolean |
false |
Align with icon items |
| css_classes |
String |
"" |
Additional CSS classes |
| html_options |
Hash |
{} |
Additional HTML attributes |
Dropdown Menu Separator
| Parameter |
Type |
Default |
Description |
| css_classes |
String |
"" |
Additional CSS classes |
| html_options |
Hash |
{} |
Additional HTML attributes |
Dropdown Menu Group
| Parameter |
Type |
Default |
Description |
| css_classes |
String |
"" |
Additional CSS classes |
| html_options |
Hash |
{} |
Additional HTML attributes |
Dropdown Menu Shortcut
| Parameter |
Type |
Default |
Description |
| text |
String |
nil |
Shortcut text |
| content |
String |
nil |
Captured HTML via capture, or use block |
| css_classes |
String |
"" |
Additional CSS classes |
| html_options |
Hash |
{} |
Additional HTML attributes |
Builder Helper
The dropdown_menu helper builds the whole menu — trigger, content, items, separators, and shortcuts — without composing partials by hand:
<%= dropdown_menu do |menu| %>
<% menu.trigger do %>
<%= icon_for :more_horizontal %>
<% end %>
<% menu.content align: :end, width: :md do %>
<% menu.label "Actions" %>
<% menu.item "Edit", href: edit_path, icon: :pencil do |item| %>
<% item.shortcut "⌘E" %>
<% end %>
<% menu.separator %>
<% menu.item "Delete", href: delete_path, method: :delete, variant: :destructive, icon: :trash %>
<% end %>
<% end %>
For data-driven menus, dropdown_menu_simple renders trigger and items from a list:
<%= dropdown_menu_simple "Actions", items: [
{ label: "Edit", href: edit_path, icon: :pencil },
{ label: "Delete", href: delete_path, method: :delete, destructive: true }
] %>
| Builder Method |
Description |
| menu.trigger(variant:, size:, as_child:, &block) |
Renders the trigger button |
| menu.content(align:, side:, width:, &block) |
Positioned menu container |
| menu.item(label, href:, method:, icon:, variant:, disabled:, &block) |
Menu item; yields an item builder for shortcut(text) |
| menu.label(text, inset:) |
Section heading |
| menu.separator / menu.group(&block) |
Divider / logical grouping |