Navigation Menu
Top-level site navigation with hover/focus dropdown panels per item.
Installation
bin/rails g wabi:add navigation_menuExample
render Components::UI::NavigationMenu.new do
render Components::UI::NavigationMenuList.new do
render Components::UI::NavigationMenuItem.new(value: "products") do
render Components::UI::NavigationMenuTrigger.new(value: "products") { "Products" }
render Components::UI::NavigationMenuContent.new(value: "products") do
render Components::UI::NavigationMenuLink.new(value: "products", href: "#") { "Analytics" }
render Components::UI::NavigationMenuLink.new(value: "products", href: "#") { "Automation" }
end
end
render Components::UI::NavigationMenuItem.new(value: "company") do
render Components::UI::NavigationMenuTrigger.new(value: "company") { "Company" }
render Components::UI::NavigationMenuContent.new(value: "company") do
render Components::UI::NavigationMenuLink.new(value: "company", href: "#") { "About" }
render Components::UI::NavigationMenuLink.new(value: "company", href: "#") { "Careers" }
end
end
end
end
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
app/components/ui/navigation_menu_link.rb
# 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).