docs/developer/admin/navigation.mdx
Spree Admin Dashboard provides a flexible navigation system that allows you to easily extend the sidebar navigation with your own menu items without modifying the core codebase. This enables safe updates while maintaining your customizations.
<Info> Starting with Spree 5.2, the navigation system uses a declarative API accessible via `Spree.admin.navigation`. This allows you to programmatically add, modify, and remove navigation items directly in your initializers. </Info>Add navigation items in your config/initializers/spree.rb file:
Rails.application.config.after_initialize do
sidebar_nav = Spree.admin.navigation.sidebar
sidebar_nav.add :brands,
label: :brands,
url: :admin_brands_path,
icon: 'award',
position: 35,
active: -> { controller_name == 'brands' },
if: -> { can?(:manage, Spree::Brand) }
end
All navigation items support the following options:
<ParamField path="label" type="Symbol or String" required> The text label for the navigation item. Can be a symbol (translation key using `Spree.t`) or a string. </ParamField> <ParamField path="url" type="Symbol or Lambda" required> The URL for the navigation item. Can be a route helper symbol (`:admin_brands_path`) or a lambda returning a URL. </ParamField> <ParamField path="icon" type="String"> Icon name from [Tabler Icons](https://tabler.io/icons). </ParamField> <ParamField path="position" type="Integer" default="0"> Numeric position in the menu. Lower numbers appear first. </ParamField> <ParamField path="active" type="Lambda"> Lambda to determine if the link should be highlighted as active. Receives view context. </ParamField> <ParamField path="if" type="Lambda"> Conditional display logic. The item only appears if this lambda returns true. Has access to view context helpers like `can?`, `current_store`, etc. </ParamField> <ParamField path="badge" type="String or Lambda"> Badge text/count to display next to the label. Can be a string or lambda that returns a value. </ParamField> <ParamField path="badge_class" type="String"> CSS class for badge styling (e.g., `'badge-info'`, `'badge-warning'`). </ParamField> <ParamField path="tooltip" type="String"> Tooltip text shown on hover. </ParamField> <ParamField path="target" type="String"> Link target attribute (e.g., `'_blank'` to open in a new tab). </ParamField> <ParamField path="section_label" type="String"> Creates a section divider with the given label instead of a clickable link. </ParamField> <ParamField path="parent" type="Symbol"> The key of an existing navigation item to nest this item under. This is the simplest way to add items to existing submenus. </ParamField>The navigation system supports multiple contexts. Spree provides predefined contexts for common use cases, and you can register custom contexts for your specific needs.
The main sidebar navigation:
sidebar_nav = Spree.admin.navigation.sidebar
Navigation in the Settings area:
settings_nav = Spree.admin.navigation.settings
Spree provides several predefined tab contexts for common admin pages:
tax_tabs = Spree.admin.navigation.tax_tabs
shipping_tabs = Spree.admin.navigation.shipping_tabs
team_tabs = Spree.admin.navigation.team_tabs
stock_tabs = Spree.admin.navigation.stock_tabs
returns_tabs = Spree.admin.navigation.returns_tabs
developers_tabs = Spree.admin.navigation.developers_tabs
audit_tabs = Spree.admin.navigation.audit_tabs
Use register_context to create a new navigation context:
# Returns a Spree::Admin::Navigation instance
custom_tabs = Spree.admin.navigation.register_context(:custom_tabs)
Returns: Spree::Admin::Navigation - The navigation context instance
Note: Calling register_context multiple times with the same name returns the same instance (idempotent).
You can create custom tab contexts for your own admin pages using register_context:
Rails.application.config.after_initialize do
# Register custom tab navigation for your brands page
brand_tabs = Spree.admin.navigation.register_context(:brand_tabs)
brand_tabs.add :active_brands,
label: 'Active Brands',
url: -> { spree.admin_brands_path(status: 'active') },
position: 10,
active: -> { params[:status] == 'active' }
brand_tabs.add :archived_brands,
label: 'Archived Brands',
url: -> { spree.admin_brands_path(status: 'archived') },
position: 20,
active: -> { params[:status] == 'archived' }
end
Then render the tabs in your view using the render_tab_navigation helper:
<%= render_tab_navigation(:brand_tabs) %>
You can list all registered navigation contexts:
# Returns an array of context names (symbols)
Spree.admin.navigation.contexts
# => [:sidebar, :settings, :brand_tabs, :inventory_tabs]
# Check if a context has been created
Spree.admin.navigation.context?(:brand_tabs)
# => true or false
Add a parent item, then add child items using the parent option:
sidebar_nav.add :brands,
label: :brands,
url: :admin_brands_path,
icon: 'award',
position: 35,
if: -> { can?(:manage, Spree::Brand) }
sidebar_nav.add :all_brands,
label: 'All Brands',
url: :admin_brands_path,
position: 10,
parent: :brands,
active: -> { controller_name == 'brands' }
sidebar_nav.add :brand_categories,
label: 'Brand Categories',
url: :admin_brand_categories_path,
position: 20,
parent: :brands,
active: -> { controller_name == 'brand_categories' },
if: -> { can?(:manage, Spree::BrandCategory) }
sidebar_nav = Spree.admin.navigation.sidebar
products_nav = sidebar_nav.find(:products)
Use the parent option to add an item to an existing submenu:
sidebar_nav.add :brands,
label: :brands,
url: :admin_brands_path,
position: 50,
parent: :products,
active: -> { controller_name == 'brands' },
if: -> { can?(:manage, Spree::Brand) }
sidebar_nav.remove(:vendors)
sidebar_nav.update(:products, label: 'Catalog', icon: 'shopping-cart')
sidebar_nav.replace(:products, label: 'Products', icon: 'package') do |products|
# Define new submenu structure
end
# Move to specific position
sidebar_nav.move(:brands, position: 25)
# Move before another item
sidebar_nav.move(:brands, before: :products)
# Move after another item
sidebar_nav.move(:brands, after: :products)
# Move to first position
sidebar_nav.move(:brands, position: :first)
# Move to last position
sidebar_nav.move(:brands, position: :last)
sidebar_nav.add :orders,
label: :orders,
url: :admin_orders_path,
icon: 'inbox',
position: 20,
active: -> { controller_name == 'orders' },
if: -> { can?(:manage, Spree::Order) },
badge: -> {
count = Spree::Order.ready_to_ship.count
count if count.positive?
},
badge_class: 'badge-warning'
sidebar_nav.add :settings_section,
section_label: 'Settings',
position: 90
sidebar_nav.add :store_settings,
label: :settings,
url: -> { spree.edit_admin_store_path(section: 'general-settings') },
icon: 'settings',
position: 100
sidebar_nav.add :vendors,
label: :vendors,
url: 'https://spreecommerce.org/marketplace-ecommerce/',
icon: 'heart-handshake',
position: 35,
if: -> { can?(:manage, current_store) && !defined?(SpreeEnterprise) },
badge: 'Enterprise',
tooltip: 'Multi-Vendor Marketplace is available in the Enterprise Edition',
target: '_blank'
sidebar_nav.add :products,
label: :products,
url: :admin_products_path,
icon: 'package',
position: 30,
active: -> {
%w[products external_categories taxons taxonomies option_types option_values
properties stock_items stock_transfers variants digital_assets].include?(controller_name)
},
if: -> { can?(:manage, Spree::Product) }
Main sidebar navigation positions:
The admin navigation system works through injection points defined throughout the sidebar. You can inject custom navigation items into these predefined locations, add new top-level menu items, or create nested submenus.
The main navigation file is located at admin/app/views/spree/admin/shared/sidebar/_store_nav.html.erb and provides several injection points:
Injects navigation items into the main sidebar navigation, after the Reports item and before the Storefront and Integrations sections.
<Frame>
</Frame>
This is the primary injection point for adding custom top-level navigation items.
Injects navigation items into the Products submenu, after the Properties item.
Use this to add product-related navigation items that logically belong under the Products section.
Injects navigation items into the Orders submenu, after the Draft Orders item.
Use this to add order-related navigation items.
Injects navigation items into the Settings section, after the Policies item.
Use this when Settings mode is active to add configuration-related items.
Injects navigation items at the end of the Settings section.
Use this to add additional settings-related navigation items.
The nav_item helper method is provided by Spree::Admin::NavigationHelper and makes it easy to create properly formatted navigation items.
nav_item(label = nil, url, icon: nil, active: nil, data: {})
<%= nav_item(Spree.t(:custom_section), spree.admin_custom_path, icon: 'star') %>
<%= nav_item(
Spree.t(:inventory),
spree.admin_inventory_path,
icon: 'boxes',
active: controller_name == 'inventory'
) %>
<%= nav_item(nil, spree.admin_dashboard_path, icon: 'home') do %>
<%= icon 'home' %>
<%= Spree.t(:dashboard) %>
<span class="badge ml-auto">New</span>
<% end %>
Let's add a new "Inventory" navigation item to the main sidebar.
mkdir -p app/views/spree/admin/shared
touch app/views/spree/admin/shared/_inventory_nav.html.erb
<% if can?(:manage, Spree::Inventory) %>
<%= nav_item(
Spree.t(:inventory),
spree.admin_inventory_index_path,
icon: 'boxes',
active: controller_name == 'inventory'
) %>
<% end %>
Add this to your config/initializers/spree.rb:
In your config/locales/en.yml:
en:
spree:
inventory: "Inventory"
Restart your web server to load the initializer changes. The navigation item should now appear in the sidebar.
To create a navigation item with a submenu, you need to use the nav-submenu class and manage the visibility based on the active state.
<% inventory_active = %w[inventory warehouses stock_movements].include?(controller_name) %>
<% if can?(:manage, Spree::Inventory) %>
<%= nav_item(
Spree.t(:inventory),
spree.admin_inventory_index_path,
icon: 'boxes',
active: inventory_active
) %>
<ul class="nav-submenu <% unless inventory_active %>d-none<% end %>">
<% if can?(:manage, Spree::Warehouse) %>
<%= nav_item(
Spree.t(:warehouses),
spree.admin_warehouses_path,
active: controller_name == 'warehouses'
) %>
<% end %>
<% if can?(:manage, Spree::StockMovement) %>
<%= nav_item(
Spree.t(:stock_movements),
spree.admin_stock_movements_path,
active: controller_name == 'stock_movements'
) %>
<% end %>
<%= render_admin_partials(:store_inventory_nav_partials) %>
</ul>
<% end %>
Active State Variable: Define a variable to track when any item in the menu group is active:
<% inventory_active = %w[inventory warehouses stock_movements].include?(controller_name) %>
Parent Navigation Item: Use the active state variable for the parent item:
<%= nav_item(..., active: inventory_active) %>
Submenu Container: Use the nav-submenu class and conditionally add d-none to hide when inactive:
<ul class="nav-submenu <% unless inventory_active %>d-none<% end %>">
Child Items: Add child navigation items within the submenu:
<%= nav_item(Spree.t(:child_item), spree.admin_child_path) %>
Nested Injection Point (Optional): Add an injection point within the submenu for further extensibility:
<%= render_admin_partials(:store_inventory_nav_partials) %>
<%= nav_item(nil, spree.admin_orders_path, icon: 'inbox', active: orders_active) do %>
<%= icon 'inbox' %>
<%= Spree.t(:orders) %>
<span class="badge ml-auto"><%= pending_orders_count %></span>
<% end %>
<% products_active = %w[products external_categories taxons taxonomies option_types option_values properties stock_items stock_transfers].include?(controller_name) || request.path.include?('products') %>
<%= nav_item(
Spree.t(:products),
spree.admin_products_path,
icon: 'package',
active: products_active
) %>
To add an item to an existing submenu (e.g., Products), use the appropriate injection point:
Create: app/views/spree/admin/shared/_custom_products_nav.html.erb
<% if can?(:manage, Spree::CustomProductFeature) %>
<%= nav_item(
Spree.t(:custom_feature),
spree.admin_custom_product_feature_path,
active: controller_name == 'custom_product_features'
) %>
<% end %>
Register in config/initializers/spree.rb:
When the admin is in "Settings mode" (the dedicated settings view), use the settings_nav_partials injection point:
<% if settings_active? %>
<!-- Settings mode is active -->
<%= nav_item(Spree.t(:custom_settings), spree.edit_admin_store_path(section: 'custom-settings'), icon: 'adjustments') %>
<% end %>
Register with:
<Tabs> <Tab title="Spree 5.2+"> ```ruby config/initializers/spree.rb Rails.application.config.after_initialize do Spree.admin.navigation.settings << 'spree/admin/shared/custom_settings_nav' end ``` </Tab> <Tab title="Spree 5.1 and below"> ```ruby config/initializers/spree.rb Rails.application.config.spree_admin.settings_nav_partials << 'spree/admin/shared/custom_settings_nav' ``` </Tab> </Tabs><% active = %w[orders shipments payments].include?(controller_name) %>
<% active = request.path.include?('products') %>
<% active = controller_name == 'dashboard' && action_name == 'show' %>
<% active = params[:section] == 'general-settings' %>