Dropdown Menu
Displays a list of actions or links that appear below a trigger button. The panel renders in the browser's top layer, so no container can clip it.
Installation
bin/rails g shadcnrb:component dropdown_menu
Usage
The block is the menu; m.trigger marks what opens it —
you write that element yourself with any helper, the same shape as
sui.tooltip and sui.hover_card.
Open/close behaviour is handled by the bundled Stimulus controller.
<%= sui.dropdown_menu do |m| %> <%= m.trigger { sui.button "Open menu", variant: :outline } %> <%= m.label "My Account" %> <%= m.separator %> <%= m.item "Profile", icon: :user %> <%= m.item "Settings", icon: :settings %> <%= m.separator %> <%= m.item "Sign out", icon: :x %> <% end %>
With links
Pass href: to m.item to render an <a> tag instead of a button.
<%= sui.dropdown_menu do |m| %> <%= m.trigger { sui.button "Navigate" } %> <%= m.item "Dashboard", href: "#" %> <%= m.item "Billing", href: "#" %> <%= m.separator %> <%= m.item "Log out", href: "#" %> <% end %>
Sides and alignment
side: / align: place the panel
(defaults :bottom / :start); it flips
to the opposite side when the viewport runs out of room. Panel options
ride in content:.
<%= sui.dropdown_menu align: :end, content: { class: "w-44" } do |m| %> <%= m.trigger { sui.button "Aligned end", variant: :outline } %> <%= m.item "Profile", "#" %> <%= m.item "Sign out", "#" %> <% end %>
Nested sub-menu
m.sub do |sub| spawns a nested menu with the same
slot shape: sub.trigger is a regular-looking item with
a right-chevron, and the rest of the block is the sub panel, floated to
the right (flipping left near the viewport edge). Opens on hover, closes
after a short delay when the pointer leaves both. Clicking a leaf item
cascades-closes every ancestor.
<%= sui.dropdown_menu do |m| %> <%= m.trigger { sui.button "File", variant: :outline } %> <%= m.item "New file", "#" %> <%= m.item "Open…", "#" %> <%= m.sub do |sub| %> <%= sub.trigger "Share" %> <%= m.item "Email link", "#" %> <%= m.item "Copy link", "#" %> <%= m.sub do |deeper| %> <%= deeper.trigger "More…" %> <%= m.item "Slack", "#" %> <%= m.item "Discord", "#" %> <% end %> <% end %> <%= m.separator %> <%= m.item "Save", "#" %> <%= m.item "Save as…", "#" %> <% end %>
Composition — scoped helpers inside
Pass a block with a scope arg (m.item do |i|) and
use i.link_to / i.button_to inside.
The item's scope tells link_to /
button_to they're in a non-top-level context, so they
default to variant: :bare automatically — no stacked
button visuals over the row styling. CSS on
m.item
([&>a]:flex-1 [&>form]:flex-1) makes the inner element
fill the row so clicks land anywhere.
<%= sui.dropdown_menu do |m| %> <%= m.trigger { sui.button "Account" } %> <%= m.item do |i| %> <%= i.link_to "Profile", "#", icon: :user %> <% end %> <%= m.item do |i| %> <%= i.link_to "Settings", "#", icon: :settings %> <% end %> <%= m.separator %> <%= m.item do |i| %> <%= i.button_to "Sign out", "#", method: :delete, icon: :x %> <% end %> <% end %>
Lazy-loaded items
Pass src: and the menu fetches its items on first
open via a Turbo Frame — handy for per-row action menus in long
tables. Same options as sui.dialog:
reload: re-fetches every open,
loading: (or the block body after the trigger) is
the loading state. The lazy partial reaches m.item
etc. through sui.dropdown_menu_proxy, and the same
shape works as a kwarg: sui.button icon: :ellipsis, dropdown_menu: { src: ... }.
<%= sui.dropdown_menu src: lazy_dropdown_items_path, content: { class: "w-48" } do |m| %> <%= m.trigger { sui.button "Row actions", variant: :outline } %> <div class="grid gap-1" aria-hidden="true"> <%= sui.skeleton class: "h-8" %> <%= sui.skeleton class: "h-8" %> <%= sui.skeleton class: "h-8" %> </div> <% end %> <%= sui.button icon: :ellipsis, variant: :ghost, "aria-label": "More", dropdown_menu: { src: lazy_dropdown_items_path, content: { class: "w-48" } } %>
API
| Method | Args | Default | Description |
|---|---|---|---|
sui.dropdown_menu | side:, align:, src:, reload:, loading:, content:, **opts | :bottom / :start | Wrapper; the block body is the menu panel. Mounts the shadcnrb--dropdown-menu--component controller (a subclass of the shared anchored engine) and owns the click/Esc wiring. **opts land on the wrapper, content: on the panel. src: lazy-loads the items via Turbo Frame. |
m.trigger | &block | — | Marks which part of the block is the trigger; everything else is the panel. Your markup, verbatim — click-to-toggle is bound on the wrapper, so the element keeps its own behaviour. Hoisted to the top of the wrapper, so its position in the block doesn't matter. |
m.label(name) | — | — | Non-interactive section heading inside the panel. |
m.separator | — | — | Horizontal divider line between groups of items. |
m.item(name) | href:, &block | nil | Interactive row. Shortcut renders <button> (or <a> when href: is given). Block form renders a <div role="menuitem"> wrapper — put link_to / button_to inside. Closes menu on click. |
m.sub | side:, align:, content:, &block | :right / :start | Nested menu, same slot shape: sub.trigger(name) renders the chevroned row, the rest of the block is the sub panel. |