Building a Dynamic Multi-Level Category Menu in Salesforce B2B LWC

In the world of B2B commerce, intuitive and scalable navigation is everything. When dealing with a wide range of product categories and subcategories, a dynamic and hierarchical menu structure becomes critical.

In this post, weโ€™ll walk through building a multi-level navigational menu using Lightning Web Components (LWC) and Apex in Salesforce, based on product categories stored in Salesforce B2B Commerce.


๐Ÿ’ก Objective

To create a Lightning Web Component that:

  • Retrieves product categories from Salesforce via Apex.
  • Dynamically renders them in a multi-tiered, collapsible menu layout.
  • Supports navigation to category-specific pages.

๐Ÿงฉ Apex Controller: MultiLevelMenuBarController

We start by creating an Apex class to retrieve the product categories marked as navigational:

public without sharing class MultiLevelMenuBarController{
    @AuraEnabled(cacheable=true)
    public static List<ProductCategory> getCategories() {
        try {
            return [SELECT ID, Name, ParentCategoryId, NumberOfProducts FROM ProductCategory WHERE IsNavigational = true];
        } catch (Exception e) {
            throw new AuraHandledException(e.getMessage());
        }
    }
}

๐Ÿ” Key Highlights:

  • The method is annotated with @AuraEnabled(cacheable=true) for efficient, reactive LWC data fetching.
  • The query filters categories that are marked navigational (IsNavigational = true).

๐ŸŽจ LWC HTML Template

Here’s the HTML structure that recursively renders up to five levels of category hierarchy:

<template>
    <lightning-layout class="menubar-layout">
        <template for:each={parentCategories} for:item="category">
            <lightning-layout-item key={category.Id}  padding="around-small" class="parent-category">
                <div class="categoryName" onclick={navigateToCategory} data-id={category.Id}>{category.Name}</div>
                <div class="child-dropdown">
                    <lightning-layout multiple-rows class="slds-var-m-vertical_small">
                        <lightning-layout-item key={category.Id} for:each={category.child} for:item="children1" size="3" class="slds-var-p-bottom_x-small slds-var-p-left_small"> 
                            <div class="slds-var-p-bottom_x-small categoryName" onclick={navigateToCategory} data-id={children1.Id}><b>{children1.Name}</b></div>
                            <div key={children1.Id} for:each={children1.child} for:item="children2">
                                <div class="slds-var-p-bottom_x-small slds-var-p-left_medium slds-truncate categoryName" onclick={navigateToCategory} data-id={children2.Id}>{children2.Name}</div>
                                <div key={children2.Id} for:each={children2.child} for:item="children3">
                                    <div class="slds-var-p-bottom_x-small slds-var-p-left_large slds-truncate categoryName" onclick={navigateToCategory} data-id={children3.Id}>{children3.Name}</div>
                                    <div key={children3.Id} for:each={children3.child} for:item="children4">
                                        <div class="slds-var-p-bottom_x-small slds-var-p-left_x-large slds-truncate categoryName" onclick={navigateToCategory} data-id={children4.Id}>{children4.Name}</div>
                                    </div>
                                </div>
                            </div>
                        </lightning-layout-item>
                    </lightning-layout>
                </div>
            </lightning-layout-item>
        </template>
    </lightning-layout>
</template>

๐Ÿง  LWC JavaScript Logic

The JavaScript handles data transformation and UI logic.

import { LightningElement } from 'lwc';
import { track, wire } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';

import getCategories from '@salesforce/apex/MultiLevelMenuBarController.getCategories';

export default class B2blMenuBar extends NavigationMixin(LightningElement)  {
    
    @track
    parentCategories = [];
    @track 
    categoriesJSON = [];

    @wire(getCategories)
    categories({ error, data }) {
      if (data) {
        console.log({data});
        var allCategories = JSON.parse(JSON.stringify(data));
        var tempparentCategories = allCategories.filter(tempdata => tempdata.ParentCategoryId == undefined);
        console.log('temp parentCategories::', JSON.parse(JSON.stringify(tempparentCategories)));
        let categoryJSON = {};
        tempparentCategories.forEach((cat, index) => { //level 1
            
            var childCate1 = allCategories.filter(tempcat => tempcat.ParentCategoryId == cat.Id);
            console.log('childCate1::', childCate1);
            
            if(childCate1.length>0){
                
                var tempCat = JSON.parse(JSON.stringify(cat));
                tempCat['child'] = childCate1;
                tempparentCategories[index] = tempCat;
                console.log('first level cat::', cat);
                    
                childCate1.forEach((cat1, index) =>{ //level 2
                    console.log('cat1::', JSON.parse(JSON.stringify(cat1)));
                    var childCate2 = allCategories.filter(tempcat2 => tempcat2.ParentCategoryId == cat1.Id);
                    console.log('childCate2::', childCate2);
                    
                    if(childCate2.length>0){
                        
                        var tempCat1 = JSON.parse(JSON.stringify(cat1));
                        tempCat1['child'] = childCate2;
                        childCate1[index] = tempCat1;
                        console.log('childCate::', childCate1);
                        
                        childCate2.forEach((cat2, index) =>{ //level 3
                            
                            var childCate3 = allCategories.filter(tempcat3 => tempcat3.ParentCategoryId == cat2.Id);
                            console.log('childCate3::', childCate3);
                            
                            if(childCate3.length>0){
                                var tempCat2 = JSON.parse(JSON.stringify(cat2));
                                tempCat2['child'] = childCate3;
                                childCate2[index] = tempCat2;
                                console.log('childCate::', childCate2);

                                childCate3.forEach((cat3,index) =>{ //level 4
                                    
                                    var childCate4 = allCategories.filter(tempcat4 => tempcat4.ParentCategoryId == cat3.Id);
                                    console.log('childCate4::', childCate4);
                                    
                                    if(childCate4.length>0){
                                        var tempCat3 = JSON.parse(JSON.stringify(cat3));
                                        tempCat3['child'] = childCate4;
                                        childCate3[index] = tempCat3;
                                        console.log('childCate::', childCate3);

                                        childCate4.forEach((cat4, index) =>{ //level 5
                                            
                                            var childCate5 = allCategories.filter(tempcat5 => tempcat5.ParentCategoryId == cat4.Id);
                                            console.log('childCate5::', childCate5);
                                            
                                            if(childCate5.length>0){
                                                var tempCat4 = JSON.parse(JSON.stringify(cat4));
                                                tempCat4['child'] = childCate5;
                                                childCate4[index] = tempCat4;
                                                console.log('childCate::', childCate4);
                                            }
                                            
                                        });
                                    }
                                });
                            }                
                        }); 
                    }
                });
            }
        });
        this.categoriesJSON = categoryJSON;
        this.parentCategories = tempparentCategories;
        console.log('final parentCategories::', JSON.parse(JSON.stringify(this.parentCategories)));

        console.log('categoriesJSON::', JSON.parse(JSON.stringify(this.categoriesJSON)));
      } else if (error) {
        console.log({error});
      }
    }

    navigateToCategory(event){
        var catId = event.currentTarget.dataset.id;
        console.log({catId});
        this[NavigationMixin.Navigate]({
            type: 'standard__webPage',
            attributes: {
                url: '/category/' + catId
            }
        })
    }
  
}

๐Ÿ”„ Highlights:

  • Navigations are handled using NavigationMixin.

๐Ÿงพ Meta Configuration

The component is exposed to App Pages and Community Pages:

&lt;LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    &lt;apiVersion>56.0&lt;/apiVersion>
    &lt;isExposed>true&lt;/isExposed>
    &lt;targets>
        &lt;target>lightning__AppPage&lt;/target>
        &lt;target>lightningCommunity__Page&lt;/target>
        &lt;target>lightningCommunity__Default&lt;/target>
    &lt;/targets>
&lt;/LightningComponentBundle>


๐Ÿ”š Final Thoughts

This solution allows you to:

  • Dynamically display deeply nested categories.
  • Reuse the component across multiple experiences (App/Page/Community).
  • Ensure a scalable and navigable user experience.


This site uses cookies to offer you a better browsing experience. By browsing this website, you agree to our use of cookies.