Phlexi::Menu is a flexible and powerful menu builder for Ruby applications. It provides an elegant way to create hierarchical menus with support for icons, badges, and active state detection.
Table of Contents
Features
- Hierarchical menu structure with intelligent depth control
- Enhanced badge system with customizable options and wrappers
- Intelligent active state detection
- Flexible theming system with depth awareness
- Smart nesting behavior based on depth limits
- Works seamlessly with Phlex components
- Rails-compatible URL handling
- Customizable rendering components
Prerequisites
- Ruby >= 3.2.2
- Rails (optional, but recommended)
- Phlex (~> 1.11)
Installation
Add this line to your application's Gemfile:
gem 'phlexi-menu'
And then execute:
$ bundle install
Usage
Basic Usage
class MainMenu < Phlexi::Menu::Component
class Theme < Theme
def self.theme
super.merge({
nav: "bg-white shadow",
items_container: "space-y-1",
item_wrapper: ->(depth) { "relative pl-#{depth * 4}" },
item_link: "flex items-center px-4 py-2 hover:bg-gray-50",
item_label: ->(depth) { "mx-3 text-gray-#{600 + (depth * 100)}" },
leading_badge_wrapper: "flex items-center",
trailing_badge_wrapper: "flex items-center ml-auto",
leading_badge: "mr-2 px-2 py-0.5 text-xs rounded-full bg-blue-100 text-blue-600",
trailing_badge: "px-2 py-0.5 text-xs rounded-full bg-red-100 text-red-600",
icon: "h-5 w-5",
active: "bg-blue-50 text-blue-600"
})
end
end
end
menu = Phlexi::Menu::Builder.new do |m|
m.item "Dashboard", url: "/", icon: DashboardIcon
m.item "Users", url: "/users"
.with_leading_badge("Beta", color: "blue")
.with_trailing_badge("23", size: "sm") do |users|
users.item "All Users", url: "/users"
users.item "Add User", url: "/users/new"
end
m.item "Settings",
url: "/settings",
icon: SettingsIcon,
leading_badge: StatusBadge.new(type: "warning")
end
render MainMenu.new(menu, max_depth: 2)
Menu items support several options:
m.item "Menu Item",
url: "/path",
icon: IconComponent,
leading_badge: "Beta",
trailing_badge: "99+",
active: ->(context) {
context.controller_name == "products"
}
The new fluent badge API provides a cleaner way to add badges:
m.item "Products"
.with_leading_badge("New", class: "text-blue-900")
.with_trailing_badge("99+", class: "text-sm")
Badge System
The enhanced badge system supports both simple text badges and complex component badges with customization options:
m.item "Products"
.with_leading_badge("New", class: "text-green-400")
m.item "Messages"
.with_leading_badge(StatusBadge.new(status: "active"))
.with_trailing_badge(CounterBadge.new(count: 3))
m.item "Legacy",
leading_badge: "Beta",
trailing_badge: "2",
leading_badge_options: { class: "text-green-400"},
Component Options
The menu component accepts these initialization options:
MainMenu.new(
menu,
max_depth: 3,
**options
)
Nesting and Depth Limits
Phlexi::Menu intelligently handles menu nesting based on the specified maximum depth:
menu = Phlexi::Menu::Builder.new do |m|
m.item "Level 0" do |l0|
l0.item "Level 1" do |l1|
l1.item "Level 2" do |l2|
l2.item "Level 3"
end
end
end
end
menu_component = MainMenu.new(menu, max_depth: 2)
Theming
The theming system now includes dedicated wrapper elements for badges:
def self.theme
super.merge({
leading_badge_wrapper: "flex items-center",
trailing_badge_wrapper: "ml-auto",
leading_badge: ->(depth) {
["badge", depth.zero? ? "primary" : "secondary"]
},
trailing_badge: ->(depth) {
["badge", "ml-2", "level-#{depth}"]
}
})
end
Static Theming
Basic theme configuration with fixed classes:
class CustomMenu < Phlexi::Menu::Component
class Theme < Theme
def self.theme
super.merge({
nav: "bg-white shadow rounded-lg",
items_container: "space-y-1",
item_wrapper: "relative",
item_link: "flex items-center px-4 py-2 hover:bg-gray-50",
item_span: "flex items-center px-4 py-2",
item_label: "mx-3",
leading_badge: "mr-2 px-2 py-0.5 text-xs rounded-full bg-blue-100 text-blue-600",
trailing_badge: "ml-auto px-2 py-0.5 text-xs rounded-full bg-red-100 text-red-600",
icon: "h-5 w-5",
active: "bg-blue-50 text-blue-600"
})
end
end
end
Depth-Aware Theming
Advanced theme configuration with depth-sensitive classes:
class DepthAwareMenu < Phlexi::Menu::Component
class Theme < Theme
def self.theme
super.merge({
item_wrapper: ->(depth) { "relative pl-#{depth * 4}" },
item_label: ->(depth) { "mx-3 text-gray-#{600 + (depth * 100)}" },
leading_badge: ->(depth) {
["badge", "mr-2", depth.zero? ? "primary" : "secondary"]
}
})
end
end
end
Theme values can be either:
- Static strings for consistent styling
- Arrays of classes that will be joined
- Callables (procs/lambdas) that receive the current depth and return strings or arrays
Rails Integration
In your controller:
class ApplicationController < ActionController::Base
def navigation
@navigation ||= Phlexi::Menu::Builder.new do |m|
m.item "Home",
url: root_path,
icon: HomeIcon
if user_signed_in?
m.item "Account",
url: account_path,
trailing_badge: notifications_count do |account|
account.item "Profile", url: profile_path
account.item "Settings", url: settings_path
account.item "Logout", url: logout_path
end
end
if current_user&.admin?
m.item "Admin",
url: admin_path
.with_leading_badge("Admin", variant: "warning")
end
end
end
helper_method :navigation
end
Advanced Usage
Active State Detection
The menu system provides multiple ways to determine the active state of items:
m.item "Custom Active",
url: "/path",
active: ->(context) {
context.request.path.start_with?("/path")
}
Default behavior checks:
- Custom active logic (if provided)
- Current page match
- Active state of child items
Component Customization
You can customize specific rendering steps by subclassing the base component and overriding specific methods.
The component provides these customization points:
Core Rendering Methods
render_items(items, depth)
: Handles collection of items and nestingrender_item_wrapper(item, depth)
: Wraps individual items in list elementsrender_item_content(item, depth)
: Chooses between link and span renderingrender_item_interior(item, depth)
: Handles the item's internal layout
Badge Related Methods
render_leading_badge(item, depth)
: Renders the item's leading badge with wrapperrender_trailing_badge(item, depth)
: Renders the item's trailing badge with wrapperrender_badge(badge, options, type, depth)
: Core badge rendering with options support
Other Components
render_icon(icon, depth)
: Renders the icon componentrender_label(label, depth)
: Renders the item's label
Helper Methods
nested?(item, depth)
: Determines if an item should show nested childrenactive?(item)
: Determines item's active stateactive_class(item, depth)
: Resolves active state stylingthemed(component, depth)
: Resolves theme values for componentscompute_item_wrapper_classes(item, depth)
: Computes wrapper CSS classes
Each method receives the current depth as a parameter for depth-aware rendering and theming. You can override any combination of these methods to customize the rendering behavior:
class CustomMenu < Phlexi::Menu::Component
def render_badge(badge, options, type, depth)
if badge.is_a?(String) && type == :leading_badge
render_text_badge(badge, options, depth)
else
super
end
end
private
def render_text_badge(text, options, depth)
span(class: themed(:leading_badge, depth)) do
span(class: "dot") { "•" }
text
end
end
end
For Rails applications, you can also integrate with helpers and routes:
class ApplicationMenu < Phlexi::Menu::Component
protected
def active?(item)
return super unless helpers&.respond_to?(:current_page?)
current_page?(item.url) || item.items.any? { |child| active?(child) }
end
def render_icon(icon, depth)
return super unless icon.respond_to?(:to_svg)
raw icon.to_svg(class: themed(:icon, depth))
end
end
The component's modular design allows you to customize exactly what you need while maintaining the core menu functionality.
Example of building menus based on user permissions:
Phlexi::Menu::Builder.new do |m|
m.item "Home", url: root_path
if current_user.can?(:manage, :products)
m.item "Products", url: products_path do |products|
products.item "All Products", url: products_path
products.item "Categories", url: categories_path if current_user.can?(:manage, :categories)
products.item "New Product", url: new_product_path
end
end
current_user.organizations.each do |org|
m.item org.name,
url: organization_path(org),
icon: OrgIcon,
trailing_badge: org.unread_notifications_count
end
end
Development
After checking out the repo:
- Run
bin/setup
to install dependencies - Run
bin/appraise install
to install appraisal gemfiles - Run
bin/appraise rake test
to run the tests against all supported versions - You can also run
bin/console
for an interactive prompt
For development against a single version, you can just use bundle exec rake test
.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/radioactive-labs/phlexi-menu.
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
License
The gem is available as open source under the terms of the MIT License.