ComponentsThemes
Palette
Default
Stone
Rose
Blue
Green
Violet
Yellow
Orange
6

← Components

Navigation Menu

Top-level site navigation with hover/focus dropdown panels per item.

Installation

bin/rails g wabi:add navigation_menu

Example

Source

app/components/ui/navigation_menu.rb

# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class NavigationMenu < Wabi::Base
      def initialize(id: nil, orientation: :horizontal, aria_label: nil, **attrs)
        @id          = id
        @orientation = orientation
        @aria_label  = aria_label
        @attrs       = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        nav(
          id: @id,
          "aria-label": @aria_label,
          data: {
            controller: "wabi--navigation-menu",
            "wabi--navigation-menu-orientation-value": @orientation.to_s,
          },
          class: merge_class("relative flex", user_class)
        ) do
          yield if block_given?
        end
      end
    end
  end
end

app/components/ui/navigation_menu_list.rb

# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class NavigationMenuList < Wabi::Base
      def initialize(**attrs)
        @attrs = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        ul(
          data: { "wabi--navigation-menu-target": "list" },
          class: merge_class("flex items-center gap-1", user_class)
        ) do
          yield if block_given?
        end
      end
    end
  end
end

app/components/ui/navigation_menu_item.rb

# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class NavigationMenuItem < Wabi::Base
      def initialize(value:, **attrs)
        @value = value
        @attrs = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        li(
          data: { "wabi--navigation-menu-target": "item", "wabi-value": @value.to_s },
          class: merge_class("relative", user_class)
        ) do
          yield if block_given?
        end
      end
    end
  end
end

app/components/ui/navigation_menu_trigger.rb

# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class NavigationMenuTrigger < Wabi::Base
      def initialize(value:, **attrs)
        @value = value
        @attrs = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        button(
          type: "button",
          data: { "wabi--navigation-menu-target": "trigger", "wabi-value": @value.to_s },
          class: merge_class(
            "inline-flex items-center gap-1 rounded-md px-3 py-2 text-sm font-medium " \
            "hover:bg-accent hover:text-accent-foreground data-[state=open]:bg-accent " \
            "ring-offset-background focus-visible:outline-none focus-visible:ring-2 " \
            "focus-visible:ring-ring focus-visible:ring-offset-2",
            user_class
          )
        ) do
          yield if block_given?
        end
      end
    end
  end
end

app/components/ui/navigation_menu_content.rb

# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class NavigationMenuContent < Wabi::Base
      def initialize(value:, **attrs)
        @value = value
        @attrs = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        div(
          data: { "wabi--navigation-menu-target": "content", "wabi-value": @value.to_s },
          "data-state": "closed",
          inert: true,
          class: merge_class(
            "fixed z-50 min-w-48 rounded-md border border-input bg-popover p-2 " \
            "text-popover-foreground shadow-md outline-none transition-opacity duration-200 ease-out " \
            "motion-reduce:transition-none data-[state=open]:opacity-100 data-[state=closed]:opacity-0 " \
            "data-[state=closed]:pointer-events-none",
            user_class
          )
        ) do
          yield if block_given?
        end
      end
    end
  end
end
# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class NavigationMenuLink < Wabi::Base
      def initialize(value:, href: "#", **attrs)
        @value = value
        @href  = href
        @attrs = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        a(
          href: @href,
          data: { "wabi--navigation-menu-target": "link", "wabi-value": @value.to_s },
          class: merge_class(
            "block rounded px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground " \
            "ring-offset-background focus-visible:outline-none focus-visible:ring-2 " \
            "focus-visible:ring-ring focus-visible:ring-offset-2",
            user_class
          )
        ) do
          yield if block_given?
        end
      end
    end
  end
end

Accessibility

  • Triggers expose aria-expanded; arrow keys move between items, Escape closes.
  • Closed panels are inert (removed from the tab order and a11y tree).