Introduction
Hi friends, in this article, we will see how we can use SPFx webpart or controls combined with Office UI Fabric controls and SharePoint lists to display the global or top navigation dynamically from the list item. I will be using the Application Customizer which is part of SPFx extension.
In my previous post on SPFx – Custom Left Navigation For SharePoint Online I showed you how to use the list and list items to display the navigation. I am going to use the same list to display the items in the global or top navigation. Please go through the earlier post to know more on the list schema. The menu items for the Left or Top navigation will be decided based on the Position field.
Focus on the Code
You can refer to the Part 1 post on creating the SPFx project and installing all the dependencies. In addition to the dependencies mentioned in that post, you must install @pnp/sp – v3.13.0. It would be best if you also made some project file changes to make the pnpjs version 3 work on SPFx 1.16.1. Please follow this link to make the changes.
Note: The code is similar to the Left navigation and the only difference is the design and the project template used to display the top or global navigation.
Below is the structure of the code

Below is the code for GlobalNavApplicationCustomizer.ts
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Log } from '@microsoft/sp-core-library';
import {
BaseApplicationCustomizer, PlaceholderContent, PlaceholderName
} from '@microsoft/sp-application-base';
import { SPFI, spfi, SPFx } from "@pnp/sp";
import * as strings from 'GlobalNavApplicationCustomizerStrings';
import ModernHeader, { IModernHeaderProps } from './components/ModernHeader';
import { override } from '@microsoft/decorators';
const LOG_SOURCE: string = 'GlobalNav';
/**
* If your command set uses the ClientSideComponentProperties JSON input,
* it will be deserialized into the BaseExtension.properties object.
* You can define an interface to describe it.
*/
export interface IGlobalNavApplicationCustomizerProperties {
// This is an example; replace with your own property
testMessage: string;
}
/** A Custom Action which can be run during execution of a Client Side Application */
export default class GlobalNavApplicationCustomizer
extends BaseApplicationCustomizer<IGlobalNavApplicationCustomizerProperties> {
private _topPlaceholder: PlaceholderContent | undefined;
private _sp: SPFI;
private startReactRender() {
console.log('Available placeholders: ',
this.context.placeholderProvider.placeholderNames.map(name => PlaceholderName[name]).join(', '));
// Handling the bottom placeholder
if (!this._topPlaceholder) {
this._topPlaceholder =
this.context.placeholderProvider.tryCreateContent(
PlaceholderName.Top,
{ onDispose: this._onDispose });
// The extension should not assume that the expected placeholder is available.
if (!this._topPlaceholder) {
console.error('The expected placeholder (Bottom) was not found.');
return;
}
const elem: React.ReactElement<IModernHeaderProps> = React.createElement(ModernHeader, {
sp: this._sp
});
ReactDOM.render(elem, this._topPlaceholder.domElement);
}
}
@override
public onInit(): Promise<void> {
Log.info(LOG_SOURCE, `Initialized ${strings.Title}`);
this._sp = spfi().using(SPFx(this.context));
this.context.application.navigatedEvent.add(this, this.startReactRender);
this.startReactRender();
return Promise.resolve();
}
private _onDispose(): void {
console.log('[HelloWorldApplicationCustomizer._onDispose] Disposed custom top and bottom placeholders.');
}
}
Some of the changes done to the default file are
- Imports to the pnp/sp module
- Initializing the SPFx context to communicate with SharePoint Online list
- Verifying and binding the ModernHeader custom component to the default TopPlaceHolder
As you can see from the above code structure, I have created a Components folder to hold all the custom components. In this scenario, its the ModernHeader.tsx and below is the code.
import * as React from 'react';
import { FC, useState, useEffect } from 'react';
import { SPFI } from '@pnp/sp';
import { INavLink, ICommandBarItemProps, IContextualMenuItem, CommandBar, Stack } from 'office-ui-fabric-react';
import "@pnp/sp/webs";
import "@pnp/sp/site-users/web";
import "@pnp/sp/lists/web";
import "@pnp/sp/items";
export interface IModernHeaderProps {
sp: SPFI;
}
const ModernHeader: FC<IModernHeaderProps> = (props) => {
const { sp } = props;
const [loading, setLoading] = useState<boolean>(true);
const [menuItems, setMenuItems] = useState<ICommandBarItemProps[]>(undefined);
const [selMenu, setSelMenu] = useState<string>(undefined);
const getActiveMenuItems = async (): Promise<any[]> => {
const filQuery = `IsActive eq 1 and Position eq 'Top'`;
return await sp.web.lists.getByTitle('Menus').items
.select('ID', 'Title', 'PageUrl', 'IconName', 'Sequence', 'IsParent', 'ParentMenu/Id', 'ParentMenu/Title', 'IsActive')
.expand('ParentMenu')
.filter(filQuery)();
};
const _loadTopNavigation = async (): Promise<void> => {
const menuItems: any[] = await getActiveMenuItems();
console.log("Top Menu items: ", menuItems);
if (menuItems.length > 0) {
const navLinks: IContextualMenuItem[] = [];
const navLink: ICommandBarItemProps[] = [];
if (menuItems.length > 0) {
const fil = menuItems.filter((mi: any) => mi.IsParent);
if (fil && fil.length > 0) {
fil.map((item: any) => {
const subMenus: any[] = menuItems.filter((smi: any) => !smi.IsParent && smi.ParentMenu?.Id === item.ID);
let navsubLink: IContextualMenuItem[] = [];
if (subMenus && subMenus.length > 0) {
subMenus.map((item: any) => {
if (item.PageUrl?.Url.toLowerCase() === (window.location.origin + window.location.pathname).toLowerCase())
setSelMenu(item.ID.toString());
navsubLink.push({
key: item.ID.toString(),
text: item.Title,
url: item.PageUrl?.Url,
expandAriaLabel: item.Title,
iconProps: { iconName: item.IconName },
onClick: () => { window.location.href = item.PageUrl?.Url }
});
});
if (item.PageUrl?.Url.toLowerCase() === (window.location.origin + window.location.pathname).toLowerCase())
setSelMenu(item.ID.toString());
navLinks.push({
key: item.ID.toString(),
text: item.Title,
url: item.PageUrl?.Url,
iconProps: { iconName: item.IconName },
subMenuProps: { items: navsubLink },
onClick: () => { window.location.href = item.PageUrl?.Url }
});
} else {
if (item.PageUrl?.Url.toLowerCase() === (window.location.origin + window.location.pathname).toLowerCase())
setSelMenu(item.ID.toString());
navLinks.push({
key: item.ID.toString(),
text: item.Title,
url: item.PageUrl?.Url,
iconProps: { iconName: item.IconName },
onClick: () => { window.location.href = item.PageUrl?.Url }
});
}
navsubLink = [];
});
}
setMenuItems(navLinks);
}
}
};
useEffect(() => {
(async () => {
await _loadTopNavigation();
})();
}, []);
return (
<div>
<CommandBar
items={menuItems}
/>
</div>
)
};
export default ModernHeader;
Below are the changes or functionalities implemented
- getActiveMenuItems – Method to retrieve the active Top menu items from the list.
- _loadTopNavigation – To massage the data received from the list items to fit in to the CommandBar control.
- CommandBar – Office UI Fabric navigation control used to display the top navigation
Note: No customization done to the ClientSideInstance.xml or elments.xml

Conclussion
As you can see from the code explained above, it’s straightforward and easy to implement custom navigation. I hope you enjoyed and learned some new things from this article. See you next time in another interesting article.
Happy Coding…