SPFx Application Customizer: Build Global Headers and Footers for SharePoint Online (2026)
Add a branded global header and footer to every modern SharePoint page using an SPFx Application Customizer extension — with tenant-wide deployment via PnP PowerShell.

The Problem SharePoint Add-in Retirement Created
On April 2, 2026, Microsoft officially retired SharePoint Add-ins. Every SharePoint Add-in stopped working across all tenants — no extensions, no exceptions. (Microsoft Learn)
If your organization had a custom branded header or footer deployed via an Add-in, it stopped rendering that day. The only supported replacement is a SharePoint Framework Application Customizer extension — and if you have not migrated yet, this guide will walk you through building one from scratch in 2026.
Even if you were never on Add-ins, Application Customizers are the right tool whenever you need something to appear on every page across your SharePoint tenant: a cookie consent banner, a global navigation bar, an emergency announcement strip, or a branded footer with links and contact information.
---
Key Takeaways
- Application Customizers are SPFx extensions that inject HTML into two page-level placeholders:
PageHeaderandPageFooter.
- They run on every modern SharePoint page — site pages, list views, document libraries — without any per-page configuration.
- Tenant-wide deployment via the Tenant Wide Extensions list pushes your customizer to all sites automatically, without requiring site owners to install anything.
- The April 2026 SharePoint Add-in retirement deadline means any organization still relying on Add-in-based branding needs to migrate now.
- In SPFx 1.22+, new extension projects use the Heft build system instead of Gulp. The scaffolding and build commands changed.
- Field Customizers are being retired on June 30, 2026 — but Application Customizers and Command Sets are unaffected and fully supported.
---
What Is an SPFx Application Customizer?
The SharePoint Framework offers four types of extensions that run inside the SharePoint page itself, outside the boundaries of a web part zone:
| Extension Type | Purpose | Status |
|---|---|---|
| Application Customizer | Page-level scripts and HTML via placeholders | Supported |
| Command Set | Buttons in list/library toolbars and context menus | Supported |
| Form Customizer | Override new/edit/view forms in lists | Supported |
| Field Customizer | Custom column rendering in list views | Retiring June 30, 2026 |
Application Customizers sit at the top of this list because they have the broadest surface area: your code runs on every page load across every site in the tenant.
The two available placeholders are:
PageHeader— the strip at the very top of every SharePoint modern page, above the suite bar
PageFooter— the strip at the bottom of every page, below page content
You get a --- Tens of millions of users interact with custom SPFx solutions on a daily basis in Microsoft 365. (Microsoft 365 Developer Blog) Application Customizers are among the most commonly deployed extension types because the use cases are universal: Header use cases: Footer use cases: Any of these would previously have been deployed as a SharePoint Add-in or a JavaScript injection via a user custom action. Both approaches are now retired. Application Customizer is the supported path forward. --- Before scaffolding, confirm your environment matches current requirements. Required software: Check your Node version: If you are on Node 18 or 20, use nvm-windows to switch: Install the SPFx toolchain (SPFx 1.23 + Heft): If you have already migrated to the new SPFx CLI preview (introduced in SPFx 1.23), you can use that instead. See the Yeoman to SPFx CLI migration guide for the new workflow. --- With SPFx 1.22 and later, new projects use Heft instead of Gulp. The Yeoman generator still works but the build scripts have changed. See the full Gulp-to-Heft migration guide if you are upgrading an existing project rather than starting fresh. Run the Yeoman generator: When prompted: The --- Open export interface IContosoHeaderFooterApplicationCustomizerProperties { export default class ContosoHeaderFooterApplicationCustomizer private _headerPlaceholder: PlaceholderContent | undefined; @override private _renderPlaceholders(): void { if (this._headerPlaceholder) { // --- FOOTER --- if (this._footerPlaceholder) { private _escapeHtml(text: string): string { SharePoint renders placeholders lazily. On some pages, --- Notice the interface Example JSON you would put in This pattern keeps your extension generic while allowing per-tenant or per-site configuration. The same compiled --- Starting in January 2026, SharePoint Online enforces The safe approach in SPFx extensions is to use CSS Modules or import styles via // Then reference in your HTML template SPFx compiles SCSS modules into hashed class names at build time and injects a --- With Heft (SPFx 1.22+), the build commands changed from Gulp. Your Build for production: Then package: This generates a --- After deployment, go to the Tenant Wide Extensions list in the App Catalog site collection ( For repeatable, scriptable deployments, use PnP PowerShell. This is especially useful in CI/CD pipelines. Refer to the PnP PowerShell admin scripts guide for environment setup if you have not configured PnP PowerShell yet. # Upload and deploy the solution package # Verify deployment The -Scope Tenant Open config/package-solution.json Setting skipFeatureDeployment: true --- After deployment, your Application Customizer appears in the Tenant Wide Extensions list at your App Catalog URL: Each list item represents one active extension registration. The key columns are: --- The classic SPFx Workbench ( SPFx 1.23 ships a new debugging toolbar that improves this experience significantly. (SharePoint Framework Debug Toolbar — Microsoft Learn) When you start the local server and add the Placeholder returns Header renders but footer does not: Customizer runs multiple times: private _renderPlaceholders(): void { --- One of the most powerful Application Customizer patterns is showing the current user's information in the header. Since you have access to // In onInit or _renderPlaceholders: const me = await client // Use me.displayName in your header HTML Refer to the Graph API in SPFx user profiles guide for the full authentication flow and permission configuration needed to call user endpoints. For advanced scenarios like reading SharePoint list data for dynamic navigation, also see PnP JS in SPFx for a cleaner data-access layer. --- If you are migrating from a retired SharePoint Add-in, the pattern is: The key difference from user custom actions (which injected SPFx Application Customizer in Practice: What Organizations Build
Prerequisites
node --version
# Should output v22.x.xnvm install 22
nvm use 22npm install -g @microsoft/generator-sharepoint@1.23.0Scaffold the Application Customizer
yo @microsoft/sharepointPrompt Answer Solution name contoso-header-footerTarget environment SharePoint Online only Component type Extension Extension type Application Customizer Name ContosoHeaderFooterDescription Global header and footer for Contoso intranet
The scaffolded structure will look like this:contoso-header-footer/
├── config/
│ ├── package-solution.json
│ └── serve.json
├── src/
│ └── extensions/
│ └── contosoHeaderFooter/
│ ├── ContosoHeaderFooterApplicationCustomizer.manifest.json
│ ├── ContosoHeaderFooterApplicationCustomizer.ts
│ └── loc/
│ └── myStrings.d.ts
├── package.json
└── rig.json ← new in SPFx 1.22+ (Heft)rig.json file is new in the Heft-based toolchain — it points to @microsoft/spfx-web-build-rig and replaces the old gulpfile.js.Writing the Application Customizer
ContosoHeaderFooterApplicationCustomizer.ts. The scaffolded class extends BaseApplicationCustomizer. You will override the onInit method to attach your header and footer.Basic Placeholder Implementation
import { override } from '@microsoft/decorators';
import {
BaseApplicationCustomizer,
PlaceholderContent,
PlaceholderName
} from '@microsoft/sp-application-base';
headerMessage: string;
footerText: string;
}
extends BaseApplicationCustomizer<IContosoHeaderFooterApplicationCustomizerProperties> {
private _footerPlaceholder: PlaceholderContent | undefined;
public onInit(): Promise<void> {
this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceholders);
this._renderPlaceholders();
return Promise.resolve();
}
// --- HEADER ---
if (!this._headerPlaceholder) {
this._headerPlaceholder =
this.context.placeholderProvider.tryCreateContent(PlaceholderName.Top);
}
const message: string =
this.properties.headerMessage || 'Welcome to the Contoso Intranet';
this._headerPlaceholder.domElement.innerHTML = ;
<div class="contoso-header" role="banner">
<div class="contoso-header__inner">
<a class="contoso-header__logo" href="/" aria-label="Contoso Home">
<img src="/sites/intranet/SiteAssets/logo.svg" alt="Contoso" height="32" />
</a>
<span class="contoso-header__message">${this._escapeHtml(message)}</span>
</div>
</div>
}
if (!this._footerPlaceholder) {
this._footerPlaceholder =
this.context.placeholderProvider.tryCreateContent(PlaceholderName.Bottom);
}
const year = new Date().getFullYear();
const footerText: string = this.properties.footerText || 'Contoso Corporation';
this._footerPlaceholder.domElement.innerHTML = ;
<div class="contoso-footer" role="contentinfo">
<div class="contoso-footer__inner">
<span>© ${year} ${this._escapeHtml(footerText)}. All rights reserved.</span>
<nav class="contoso-footer__links" aria-label="Footer navigation">
<a href="/sites/intranet/SitePages/Privacy.aspx">Privacy Policy</a>
<a href="/sites/intranet/SitePages/Accessibility.aspx">Accessibility</a>
<a href="https://support.contoso.com">Help Desk</a>
</nav>
</div>
</div>
}
}
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
}Why
changedEvent MattersPlaceholderName.Top or PlaceholderName.Bottom may not be available when onInit first runs — they become available later during the page load cycle. Subscribing to changedEvent ensures your callback fires again whenever the placeholder provider's state changes, so your header/footer always renders even on complex page layouts.Passing Properties from the Tenant Wide Extensions List
IContosoHeaderFooterApplicationCustomizerProperties. Properties declared here can be passed as JSON via the ClientSideComponentProperties column in the Tenant Wide Extensions list. This allows site collection administrators to customize the header message per site without changing the code.ClientSideComponentProperties:{
"headerMessage": "Internal use only — Contoso employees",
"footerText": "Contoso Legal & Compliance"
}.sppkg runs everywhere; only the properties differ.Styling Without Violating SharePoint's CSP
style-src Content Security Policy headers on all GA tenants. Inline tags injected into innerHTML will be blocked. If you have reviewed the CSP enforcement changes, you know that the fix is to use CSS Modules or load styles through the SPFx styles module.@microsoft/load-themed-styles:// Import as a CSS Module (safe under CSP)
import styles from './ContosoHeaderFooter.module.scss';
this._headerPlaceholder.domElement.innerHTML = ;
<div class="${styles.contosoHeader}" role="banner">
...
</div> tag into the page — which is CSP-compliant. Never inject a block directly into domElement.innerHTML.Build and Package
package.json scripts now look like:{
"scripts": {
"build": "heft build --clean",
"bundle": "heft build --clean -- --ship",
"package": "heft build --clean -- --ship && gulp package-solution --ship"
}
}npm run bundlegulp package-solution --ship.sppkg file in sharepoint/solution/. The separation between the build step (now Heft) and the packaging step (still a Gulp task for now) is temporary — SPFx 1.24 will complete the migration. See the Heft migration guide for full details.Deploying to SharePoint Online
Option A: Manual Upload
.sppkg to the Apps for SharePoint library/lists/TenantWideExtensions) and verify a new entry was created automatically.Option B: PnP PowerShell (Recommended)
# Connect to the App Catalog site
Connect-PnPOnline -Url "https://contoso.sharepoint.com/sites/appcatalog" -Interactive
Add-PnPApp -Path ".\sharepoint\solution\contoso-header-footer.sppkg"
-Scope Tenant
-Overwrite
-Publish
Get-PnPApp -Scope Tenant | Where-Object { $_.Title -like "contoso" } flag uploads to the tenant app catalog and automatically populates the Tenant Wide Extensions list for Application Customizer solution types when skipFeatureDeployment is set to true in package-solution.json.Configuring package-solution.json for Tenant Deployment
and confirm this flag is set:{
"solution": {
"name": "contoso-header-footer-client-side-solution",
"id": "your-solution-guid",
"version": "1.0.0.0",
"skipFeatureDeployment": true,
"isDomainIsolated": false
}
} is what enables tenant-wide deployment. Without it, site owners would need to manually install the app on each site.Tenant Wide Extensions List: Controlling Scope
https://contoso.sharepoint.com/sites/appcatalog/lists/TenantWideExtensionsColumn Purpose Title Display name for this extension registration TenantWideExtensionComponentId The GUID from your manifest TenantWideExtensionLocation ClientSideExtension.ApplicationCustomizer TenantWideExtensionListTemplate Filter to specific list types (leave blank for all pages) TenantWideExtensionWebTemplate Filter to specific site templates (e.g., GROUP#0 for modern team sites)TenantWideExtensionSequence Order when multiple customizers are active ClientSideComponentProperties JSON properties passed to your customizer
Targeting specific site templates is useful when you want different headers for communication sites versus team sites. Set TenantWideExtensionWebTemplate to SITEPAGEPUBLISHING#0 for communication sites only, or GROUP#0 for Microsoft 365 group-connected team sites only./_layouts/15/workbench.aspxDebugging the Application Customizer
) does not support extensions — it only renders web parts. To debug an Application Customizer, you test it in a real SharePoint page.?loadSPFX=true&debugManifestsFile=... query parameters to a real SharePoint page URL, the debugging toolbar now appears in-page to give you direct feedback — no more relying solely on browser console messages.config/serve.jsonLocal Debugging Steps
npm run serve
# or with Heft:
heft build --watch and update the pageUrl to a real SharePoint page in your tenant.undefinedCommon Debugging Issues
:changedEvent
The placeholder is not yet available when your code runs. Ensure you are subscribing to as shown in the code above, not just calling tryCreateContent once in onInit.PageFooter
Some SharePoint page layouts (like full-width pages without sections) do not render the placeholder. Test on a standard page with sections.this.context.msGraphClientFactory
Each navigation in modern pages triggers the page load lifecycle again. Implement a guard check:private _headerRendered = false;
if (!this._headerRendered) {
// render header
this._headerRendered = true;
}
}Connecting to Graph API Data
, you can call the Microsoft Graph API directly from your customizer — the same way you would in an SPFx web part.Get-PnPCustomAction -Scope Siteimport { MSGraphClientV3 } from '@microsoft/sp-http';
const client: MSGraphClientV3 = await this.context.msGraphClientFactory
.getClient('3');
.api('/me')
.select('displayName,jobTitle,officeLocation')
.get();Upgrading Existing Add-in-Based Headers
)