SPFx – Custom Global Navigation For SharePoint Online


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 =
					{ 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.');

			const elem: React.ReactElement<IModernHeaderProps> = React.createElement(ModernHeader, {
				sp: this._sp
			ReactDOM.render(elem, this._topPlaceholder.domElement);

	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);
		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')

	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())
									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())
								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())
								key: item.ID.toString(),
								text: item.Title,
								url: item.PageUrl?.Url,
								iconProps: { iconName: item.IconName },
								onClick: () => { window.location.href = item.PageUrl?.Url }
						navsubLink = [];

	useEffect(() => {
		(async () => {
			await _loadTopNavigation();
	}, []);

	return (

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


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.

SPFx Global Navigation

Happy Coding…


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s