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.

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

<% 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:
Rails.application.config.spree_admin.store_nav_partials << 'spree/admin/shared/inventory_nav'

Step 4: Add Translations

In your 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:
Rails.application.config.spree_admin.store_products_nav_partials << '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:
Rails.application.config.spree_admin.settings_nav_partials << '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
I