Back to Spree

Extending Admin Navigation

docs/developer/admin/navigation.mdx

5.4.223.1 KB
Original Source

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>

Basic Usage

Add navigation items in your config/initializers/spree.rb file:

ruby
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:

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

ruby
sidebar_nav = Spree.admin.navigation.sidebar

Settings Navigation

Navigation in the Settings area:

ruby
settings_nav = Spree.admin.navigation.settings

Page Tab Navigation

Spree provides several predefined tab contexts for common admin pages:

ruby
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:

ruby
# Returns a Spree::Admin::Navigation instance
custom_tabs = Spree.admin.navigation.register_context(:custom_tabs)
<ParamField path="name" type="Symbol or String" required> The unique name for the navigation context. Will be converted to a symbol internally. </ParamField>

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:

ruby
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:

erb
<%= render_tab_navigation(:brand_tabs) %>
<Tip> Always register custom contexts in your initializer before accessing them. Attempting to access an unregistered context will raise a `NoMethodError`. </Tip>

Listing All Contexts

You can list all registered navigation contexts:

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

Checking If a Context Exists

ruby
# 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:

ruby
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) }
<Info> 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. </Info>

Modifying Existing Navigation

Finding Navigation Items

ruby
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:

ruby
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

ruby
sidebar_nav.remove(:vendors)

Updating Navigation Items

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

Replacing Navigation Items

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

Moving Navigation Items

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

ruby
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

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

Dynamic URLs

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

Complex Conditional Display

ruby
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

ruby
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

<CardGroup cols={2}> <Card title="Authorization" icon="shield-check"> Always use `if: -> { can?(...) }` to ensure users only see navigation items they have permission to access. </Card> <Card title="Translations" icon="language"> Use symbols for labels (e.g., `label: :brands`) to support internationalization via `Spree.t`. </Card> <Card title="Active States" icon="pointer"> Define clear active state logic using lambdas to highlight the current section properly. </Card> <Card title="Positioning" icon="list-numbers"> Use consistent position intervals (e.g., 10, 20, 30) to leave room for future additions. </Card> </CardGroup>

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

<AccordionGroup> <Accordion title="Navigation item not appearing"> - Restart your server after modifying initializers - Check authorization: ensure `if: -> { can?(...) }` returns true - Verify the item isn't hidden by a parent's `if` condition </Accordion> <Accordion title="Active state not working"> - 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 </Accordion> <Accordion title="Badge not displaying"> - 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 </Accordion> </AccordionGroup>

Previous versions

<Warning> The following documentation applies to Spree 5.1 and earlier. If you're using Spree 5.2+, please refer to the documentation above. </Warning>

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

<AccordionGroup> <Accordion title="store_nav_partials"> `store_nav_partials`
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.
</Accordion> <Accordion title="store_products_nav_partials"> `store_products_nav_partials`
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.
</Accordion> <Accordion title="store_orders_nav_partials"> `store_orders_nav_partials`
Injects navigation items into the Orders submenu, after the Draft Orders item.

Use this to add order-related navigation items.
</Accordion> <Accordion title="store_settings_nav_partials"> `store_settings_nav_partials`
Injects navigation items into the Settings section, after the Policies item.

Use this when Settings mode is active to add configuration-related items.
</Accordion> <Accordion title="settings_nav_partials"> `settings_nav_partials`
Injects navigation items at the end of the Settings section.

Use this to add additional settings-related navigation items.
</Accordion> </AccordionGroup>

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

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

Parameters

<ParamField path="label" type="String"> The text label for the navigation item. Can be HTML-safe content. </ParamField> <ParamField path="url" type="String"> The URL the navigation item links to. Use the `spree.` route helper prefix. </ParamField> <ParamField path="icon" type="String" default="nil"> Optional icon name from [Tabler Icons](https://tabler.io/icons). The icon will be displayed before the label. </ParamField> <ParamField path="active" type="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. </ParamField> <ParamField path="data" type="Hash" default="{}"> Additional data attributes to add to the link element. </ParamField>

Basic Usage

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

With Active State

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

With Block Content

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

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

Step 2: Add Navigation Code

erb
<% if can?(:manage, Spree::Inventory) %>
  <%= nav_item(
    Spree.t(:inventory),
    spree.admin_inventory_index_path,
    icon: 'boxes',
    active: controller_name == 'inventory'
  ) %>
<% end %>
<Tip> Always wrap your navigation items with authorization checks using `can?()` to ensure users only see menu items they have permission to access. </Tip>

Step 3: Register the Partial

Add this to your config/initializers/spree.rb:

<Tabs> <Tab title="Spree 5.2+"> ```ruby config/initializers/spree.rb Rails.application.config.after_initialize do Spree.admin.navigation.store << 'spree/admin/shared/inventory_nav' end ``` </Tab> <Tab title="Spree 5.1 and below"> ```ruby config/initializers/spree.rb Rails.application.config.spree_admin.store_nav_partials << 'spree/admin/shared/inventory_nav' ``` </Tab> </Tabs>

Step 4: Add Translations

In your config/locales/en.yml:

yaml
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

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

    erb
    <% inventory_active = %w[inventory warehouses stock_movements].include?(controller_name) %>
    
  2. Parent Navigation Item: Use the active state variable for the parent item:

    erb
    <%= nav_item(..., active: inventory_active) %>
    
  3. Submenu Container: Use the nav-submenu class and conditionally add d-none to hide when inactive:

    erb
    <ul class="nav-submenu <% unless inventory_active %>d-none<% end %>">
    
  4. Child Items: Add child navigation items within the submenu:

    erb
    <%= 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:

    erb
    <%= render_admin_partials(:store_inventory_nav_partials) %>
    

Advanced Examples

Navigation with Badge

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

Navigation with Complex Active Logic

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

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:

<Tabs> <Tab title="Spree 5.2+"> ```ruby config/initializers/spree.rb Rails.application.config.after_initialize do Spree.admin.navigation.store_products << 'spree/admin/shared/custom_products_nav' end ``` </Tab> <Tab title="Spree 5.1 and below"> ```ruby config/initializers/spree.rb Rails.application.config.spree_admin.store_products_nav_partials << 'spree/admin/shared/custom_products_nav' ``` </Tab> </Tabs>

When the admin is in "Settings mode" (the dedicated settings view), use the settings_nav_partials injection point:

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

Best Practices

<CardGroup cols={2}> <Card title="Authorization" icon="shield-check"> Always use `can?()` checks to ensure users only see navigation items they have permission to access. </Card> <Card title="Translations" icon="language"> Use `Spree.t()` for all navigation labels to support internationalization. </Card> <Card title="Icons" icon="icons"> Use consistent icons from [Tabler Icons](https://tabler.io/icons) that match Spree's design language. </Card> <Card title="Active States" icon="pointer"> Define clear active state logic to highlight the current section in the navigation. </Card> <Card title="Route Helpers" icon="route"> Always use `spree.` prefixed route helpers to reference admin routes correctly. </Card> <Card title="Injection Points" icon="puzzle"> Add your own injection points in submenus to allow further extensions by other developers. </Card> </CardGroup>

Common Patterns

Multiple Controller Check

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

Path-based Check

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

Controller and Action Check

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

Parameters-based Check

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

Troubleshooting

<AccordionGroup> <Accordion title="Navigation item not appearing"> - Ensure you've restarted your web server after adding the initializer - Check that the authorization check (`can?()`) is passing - Verify the partial path is correct (without `_` prefix and `.html.erb` suffix) - Check that the route helper exists and is correct </Accordion> <Accordion title="Icon not displaying"> - Verify the icon name exists in [Tabler Icons](https://tabler.io/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'` </Accordion> <Accordion title="Submenu not showing/hiding properly"> - Ensure the active state variable includes all relevant controller names - Check that the `nav-submenu` class is applied to the `<ul>` element - Verify the `d-none` class is conditionally added when not active - Make sure the parent nav item uses the same active state variable </Accordion> <Accordion title="Translation missing"> - 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 </Accordion> </AccordionGroup>