Extensions: Code That Wraps the Whole Site

Lesson 6: Extensions โ€” Code That Wraps the Whole Site

Web parts live inside pages. SPFx extensions live around them: code that runs on every page of a site, or that changes how lists behave. If you need a global banner, a custom column, or a toolbar button, an extension is the tool โ€” and it ships in the exact same .sppkg pipeline as web parts.

The three extension types

TypeUse caseScope once installed
Application customizerInject HTML/scripts into the header or footer placeholders of every modern page โ€” banners, navigation tweaks, analytics tags.Runs automatically across the site collection where the app is installed.
Field customizerRender a column's cells with custom UI (progress bars, status pills, images) instead of plain text.Wired to specific list columns via the column's client-side component settings / provisioning.
List view command setAdd custom buttons to the list toolbar and row context menu โ€” "Approve", "Export", custom workflows.Attached to specific list views when you configure the command set.

In other words: application customizers are site-wide by nature; field customizers and command sets are opt-in per list and are usually attached during provisioning (which is why Lesson 2's tools earn their keep).

A real application customizer

Scaffold one with the generator (yo @microsoft/sharepoint โ†’ Application Customizer) and you get a class like this โ€” here extended to show a dismissible notice bar in the top placeholder:

import { Log } from '@microsoft/sp-core-library';
import {
  BaseApplicationCustomizer,
  PlaceholderName
} from '@microsoft/sp-application-base';

export interface INoticeBarApplicationCustomizerProperties {
  message: string;   // configurable: set when you register the extension
}

export default class NoticeBarApplicationCustomizer
  extends BaseApplicationCustomizer<INoticeBarApplicationCustomizerProperties> {

  public onInit(): Promise<void> {
    Log.info('NoticeBar', 'initialized');
    // Placeholders appear async โ€” render when the top region is ready
    this.context.placeholderProvider.changedEvent.add(this, this._render);
    this._render();
    return Promise.resolve();
  }

  private _render(): void {
    const placeholder = this.context.placeholderProvider
      .tryCreateContent(PlaceholderName.Top, { onDispose: null });

    if (!placeholder) { return; }   // top slot already taken by another extension

    const message = this.properties.message || 'Default notice';
    placeholder.domElement.innerHTML = `
      <div style="background:#fde7e9;color:#a4262c;padding:8px 16px;
                  font-family:Segoe UI,sans-serif;font-size:14px;text-align:center">
        ${message}
      </div>`;
  }
}

What to notice

  • onInit() is the extension lifecycle hook โ€” the right place to subscribe and render once.
  • placeholderProvider exposes the page's injection slots (PlaceholderName.Top / .Bottom). Only one extension can own a slot; tryCreateContent returns undefined if it's taken โ€” always handle that.
  • Properties travel with registration: unlike web parts, an app customizer's message isn't set by a property pane โ€” you provide it when registering/installing the extension (via the component's properties during provisioning). The code must default gracefully.
Restraint is a feature: an application customizer runs on every page load in scope. A heavy script or a slow call there taxes the whole site, not one page. Keep customizer code lean, and prefer web parts for anything page-specific.
Classic pattern: use an application customizer to inject a single config point (e.g. a custom header or a status banner driven by a list), and let web parts stay dumb. Site-wide "chrome" belongs in the customizer; content belongs in web parts.

๐Ÿง  Knowledge Check

1. Which extension type runs automatically across a site collection once its app is installed?

2. What does tryCreateContent(PlaceholderName.Top) return if another extension already owns the top placeholder?

3. A field customizer and a command set both need what after the app is installed?

Further Reading