Senren UI
Components / Dropdown Menu

Dropdown Menu

Stable Stimulus: senren--dropdown-menu

contextual actions on rows

Usage example #

Copy this ERB into a Rails view after installing the component. The snippet below is the same code used by the live preview above.

app/views/.../dropdown_menu_example.html.erb
<div class="flex w-full justify-center">
  <%= render Senren::DropdownMenuComponent.new do |menu| %>
    <% menu.with_trigger do %>
      <%= render Senren::ButtonComponent.new(variant: :secondary) do %>
        <span>Open menu</span>
        <svg aria-hidden="true" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="ml-2 h-4 w-4 transition-transform duration-150 data-[state=open]:rotate-180" data-state="closed" data-senren-chevron>
          <path d="m5 8 5 5 5-5"></path>
        </svg>
      <% end %>
    <% end %>
    <% menu.with_item(href: "#") { "Profile" } %>
    <% menu.with_item(href: "#") { "Settings" } %>
    <% menu.with_item(href: "#") { "Billing" } %>
    <% menu.with_item(href: "#", destructive: true) { "Sign out" } %>
  <% end %>
</div>

Install this component #

Copy the official component into your app

This component requires Stimulus. Keep --client so the controller is copied with the ViewComponent.

Terminal
bin/rails senren:add dropdown_menu --client

Create a custom component with the same conventions

Use this when you need an app-specific component that follows Senren's ViewComponent and Stimulus structure. --client is required for this behavior.

Terminal
bin/rails generate senren:component dropdown_menu --client

Dependencies are resolved by senren:add: button.

At a glance #

Category Overlays
Class name Senren::DropdownMenuComponent
Stimulus senren--dropdown-menu
Variants default
Depends on button
Pairs with button, table, top_nav

Source #

app/components/senren/dropdown_menu_component.html.erb
<div data-controller="senren--dropdown-menu" class="relative inline-block" data-state="closed" data-senren-component="dropdown_menu">
  <div class="inline-flex cursor-pointer" data-state="closed" aria-haspopup="menu" aria-expanded="false" data-senren--dropdown-menu-target="trigger" data-action="click->senren--dropdown-menu#toggle keydown->senren--dropdown-menu#onTriggerKey">
    <%= trigger %>
  </div>

  <div data-senren--dropdown-menu-target="menu" role="menu" hidden
       class="absolute right-0 z-50 mt-2 w-56 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] text-[hsl(var(--senren-popover-foreground))] p-1 shadow-md">
    <% items.each do |it| %>
      <%= it %>
    <% end %>
  </div>
</div>
app/components/senren/dropdown_menu_component.rb
# frozen_string_literal: true

module Senren
  class DropdownMenuComponent < BaseComponent
    renders_one  :trigger
    renders_many :items, 'ItemTag'

    VARIANTS = { default: '' }.freeze
    SIZES    = { md: '' }.freeze

    def initialize(class_name: nil, **html)
      super(variant: :default, size: :md, class_name: class_name, **html)
    end

    class ItemTag < ViewComponent::Base
      def initialize(href: nil, method: nil, destructive: false, **opts)
        @href = href
        @method = method
        @destructive = destructive
        @opts = opts
      end

      def call
        klass = 'block w-full text-left px-3 py-2 text-sm rounded-sm hover:bg-[hsl(var(--senren-accent))] focus:bg-[hsl(var(--senren-accent))] outline-none cursor-pointer'
        klass += ' text-[hsl(var(--senren-destructive))]' if @destructive
        if @href
          link_to(content, @href, role: 'menuitem', method: @method, class: klass,
                                  data: { action: 'click->senren--dropdown-menu#close keydown->senren--dropdown-menu#onItemKey' })
        else
          tag.button(content, type: 'button', role: 'menuitem', class: klass,
                              data: { action: 'click->senren--dropdown-menu#close keydown->senren--dropdown-menu#onItemKey' }, **@opts)
        end
      end
    end
  end
end

AI agent rules #

Use for

  • +contextual actions on rows
  • +account menus

Avoid

  • -primary navigation - use top_nav or sidebar

Accessibility #

  • role=menu with role=menuitem children.
  • Arrow keys to navigate; Escape to close; type-ahead optional.