Skip to main content
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.
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.

Basic Usage

Add navigation items in your config/initializers/spree.rb file:
config/initializers/spree.rb
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

Available Options

All navigation items support the following options:
label
Symbol or String
required
The text label for the navigation item. Can be a symbol (translation key using Spree.t) or a string.
url
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.
icon
String
Icon name from Tabler Icons.
position
Integer
default:"0"
Numeric position in the menu. Lower numbers appear first.
active
Lambda
Lambda to determine if the link should be highlighted as active. Receives view context.
if
Lambda
Conditional display logic. The item only appears if this lambda returns true. Has access to view context helpers like can?, current_store, etc.
badge
String or Lambda
Badge text/count to display next to the label. Can be a string or lambda that returns a value.
badge_class
String
CSS class for badge styling (e.g., 'badge-info', 'badge-warning').
tooltip
String
Tooltip text shown on hover.
target
String
Link target attribute (e.g., '_blank' to open in a new tab).
section_label
String
Creates a section divider with the given label instead of a clickable link.
parent
Symbol
The key of an existing navigation item to nest this item under. This is the simplest way to add items to existing submenus.
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

Settings Navigation

Navigation in the Settings area:
settings_nav = Spree.admin.navigation.settings

Page Tab Navigation

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

Registering Contexts

Use register_context to create a new navigation context:
# Returns a Spree::Admin::Navigation instance
custom_tabs = Spree.admin.navigation.register_context(:custom_tabs)
name
Symbol or String
required
The unique name for the navigation context. Will be converted to a symbol internally.
Returns: Spree::Admin::Navigation - The navigation context instance Note: Calling register_context multiple times with the same name returns the same instance (idempotent).

Custom Tab Contexts

You can create custom tab contexts for your own admin pages using register_context:
config/initializers/spree.rb
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:
app/views/spree/admin/brands/index.html.erb
<%= render_tab_navigation(:brand_tabs) %>
Always register custom contexts in your initializer before accessing them. Attempting to access an unregistered context will raise a NoMethodError.

Listing All Contexts

You can list all registered navigation contexts:
# Returns an array of context names (symbols)
Spree.admin.navigation.contexts
# => [:sidebar, :settings, :brand_tabs, :inventory_tabs]

Checking If a Context Exists

# Check if a context has been created
Spree.admin.navigation.context?(:brand_tabs)
# => true or false

Creating Submenus

Add a parent item, then add child items using the parent option:
config/initializers/spree.rb
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) }
Parent items are automatically marked as active when any of their children are active. You don’t need to manually define the active option for parent items.

Modifying Existing Navigation

Finding Navigation Items

sidebar_nav = Spree.admin.navigation.sidebar
products_nav = sidebar_nav.find(:products)

Adding to Existing Submenus

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) }

Removing Navigation Items

sidebar_nav.remove(:vendors)

Updating Navigation Items

sidebar_nav.update(:products, label: 'Catalog', icon: 'shopping-cart')

Replacing Navigation Items

sidebar_nav.replace(:products, label: 'Products', icon: 'package') do |products|
  # Define new submenu structure
end

Moving Navigation Items

# 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)

Advanced Examples

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'

Section Dividers

sidebar_nav.add :settings_section,
  section_label: 'Settings',
  position: 90

Dynamic URLs

sidebar_nav.add :store_settings,
  label: :settings,
  url: -> { spree.edit_admin_store_path(section: 'general-settings') },
  icon: 'settings',
  position: 100

Complex Conditional Display

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'

Complex Active State Logic

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) }

Best Practices

Authorization

Always use if: -> { can?(...) } to ensure users only see navigation items they have permission to access.

Translations

Use symbols for labels (e.g., label: :brands) to support internationalization via Spree.t.

Active States

Define clear active state logic using lambdas to highlight the current section properly.

Positioning

Use consistent position intervals (e.g., 10, 20, 30) to leave room for future additions.

Common Positioning Reference

Main sidebar navigation positions:
  • Getting Started: 5
  • Home: 10
  • Orders: 20
  • Returns: 25
  • Products: 30
  • Customers: 40
  • Promotions: 50
  • Reports: 60
  • Storefront: 70
  • Integrations: 80
  • Settings Section: 90
  • Settings: 100
  • Admin Users: 110

Troubleshooting

  • Ensure active: lambda returns true/false
  • Check that controller_name or other conditions match correctly
  • For submenus, ensure parent uses same active logic as children
  • Ensure the badge lambda returns a non-nil value
  • Check that the badge value is truthy (empty strings won’t display)
  • For numeric badges, ensure the count is greater than 0

Previous versions

The following documentation applies to Spree 5.1 and earlier. If you’re using Spree 5.2+, please refer to the documentation above.

How it works

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:

Available Injection Points

store_nav_partialsInjects navigation items into the main sidebar navigation, after the Reports item and before the Storefront and Integrations sections.
Main navigation injection point
This is the primary injection point for adding custom top-level navigation items.
store_products_nav_partialsInjects 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.
store_orders_nav_partialsInjects navigation items into the Orders submenu, after the Draft Orders item.Use this to add order-related navigation items.
store_settings_nav_partialsInjects navigation items into the Settings section, after the Policies item.Use this when Settings mode is active to add configuration-related items.
settings_nav_partialsInjects navigation items at the end of the Settings section.Use this to add additional settings-related navigation items.

Using the nav_item Helper

The nav_item helper method is provided by Spree::Admin::NavigationHelper and makes it easy to create properly formatted navigation items.

Method Signature

nav_item(label = nil, url, icon: nil, active: nil, data: {})

Parameters

label
String
The text label for the navigation item. Can be HTML-safe content.
url
String
The URL the navigation item links to. Use the spree. route helper prefix.
icon
String
default:"nil"
Optional icon name from Tabler Icons. The icon will be displayed before the label.
active
Boolean
default:"nil"
Manually set whether the link should be marked as active. If not specified, it will be auto-detected based on the current URL.
data
Hash
default:"{}"
Additional data attributes to add to the link element.

Basic Usage

<%= nav_item(Spree.t(:custom_section), spree.admin_custom_path, icon: 'star') %>

With Active State

<%= nav_item(
  Spree.t(:inventory),
  spree.admin_inventory_path,
  icon: 'boxes',
  active: controller_name == 'inventory'
) %>

With Block Content

<%= nav_item(nil, spree.admin_dashboard_path, icon: 'home') do %>
  <%= icon 'home' %>
  <%= Spree.t(:dashboard) %>
  <span class="badge ml-auto">New</span>
<% end %>

Adding a Simple Navigation Item

Let’s add a new “Inventory” navigation item to the main sidebar.

Step 1: Create the Partial

mkdir -p app/views/spree/admin/shared
touch app/views/spree/admin/shared/_inventory_nav.html.erb

Step 2: Add Navigation Code

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 %>
Always wrap your navigation items with authorization checks using can?() to ensure users only see menu items they have permission to access.

Step 3: Register the Partial

Add this to your config/initializers/spree.rb:
config/initializers/spree.rb
Spree.admin.navigation.store << 'spree/admin/shared/inventory_nav'

Step 4: Add Translations

In your config/locales/en.yml:
config/locales/en.yml
en:
  spree:
    inventory: "Inventory"

Step 5: Restart Your Server

Restart your web server to load the initializer changes. The navigation item should now appear in the sidebar.

Creating Nested Navigation (Submenus)

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.

Example: Adding a Nested Menu

<% 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 %>

Key Points for Submenus

  1. 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) %>
    
  2. Parent Navigation Item: Use the active state variable for the parent item:
    <%= nav_item(..., active: inventory_active) %>
    
  3. 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 %>">
    
  4. Child Items: Add child navigation items within the submenu:
    <%= nav_item(Spree.t(:child_item), spree.admin_child_path) %>
    
  5. Nested Injection Point (Optional): Add an injection point within the submenu for further extensibility:
    <%= render_admin_partials(:store_inventory_nav_partials) %>
    

Advanced Examples

<%= 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
) %>

Extending Existing Submenus

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:
config/initializers/spree.rb
Spree.admin.navigation.store_products << 'spree/admin/shared/custom_products_nav'
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:
config/initializers/spree.rb
Spree.admin.navigation.settings << 'spree/admin/shared/custom_settings_nav'

Best Practices

Authorization

Always use can?() checks to ensure users only see navigation items they have permission to access.

Translations

Use Spree.t() for all navigation labels to support internationalization.

Icons

Use consistent icons from Tabler Icons that match Spree’s design language.

Active States

Define clear active state logic to highlight the current section in the navigation.

Route Helpers

Always use spree. prefixed route helpers to reference admin routes correctly.

Injection Points

Add your own injection points in submenus to allow further extensions by other developers.

Common Patterns

Multiple Controller Check

<% active = %w[orders shipments payments].include?(controller_name) %>

Path-based Check

<% active = request.path.include?('products') %>

Controller and Action Check

<% active = controller_name == 'dashboard' && action_name == 'show' %>

Parameters-based Check

<% active = params[:section] == 'general-settings' %>

Troubleshooting

  • Verify the icon name exists in Tabler Icons
  • Check that you’re using the correct parameter name: icon: not icon_name:
  • Ensure the icon name is a string, e.g., icon: 'boxes'
  • Add the translation key to your locale file
  • Ensure the locale file is in the correct location
  • Restart your server after adding translations
  • Check for typos in the translation key