logo
Published on

SPFx Web Part vs Application Customizer vs Field Customizer vs Command Set

You've scaffolded a new SPFx project and hit the first decision: which extension type do I actually need? Picking the wrong one means refactoring later β€” and each type has its own deployment model, rendering context, and limitations.

This article breaks down all four SPFx extension types with concrete use cases, so you can make the right call before writing a single line of component code.


πŸ—ΊοΈ The Four SPFx Extension Types at a Glance

SPFx gives you four building blocks. They are not interchangeable β€” each targets a specific surface area on a SharePoint page.

TypeRenders whereTypical use case
Web PartPage canvas (zones)Dashboards, forms, data display
Application CustomizerHeader / Footer placeholdersGlobal nav, notifications, banners
Field CustomizerList column cellsCustom column rendering
Command SetList toolbar / item context menuBulk actions, custom buttons

🧩 Web Part

A web part is the most familiar extension type. It renders inside a page canvas zone and is added by page editors via the web part picker. Each instance has its own property pane for configuration.

Use a web part when:

  • You need page authors to control placement
  • The component needs user-configurable properties
  • You're building a standalone UI β€” a dashboard tile, a form, a data table
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { IPropertyPaneConfiguration, PropertyPaneTextField } from '@microsoft/sp-property-pane';
import MyComponent from './components/MyComponent';
import { IMyComponentProps } from './components/IMyComponentProps';

export interface IMyWebPartProps {
  title: string;
}

export default class MyWebPart extends BaseClientSideWebPart<IMyWebPartProps> {
  public render(): void {
    const element = React.createElement<IMyComponentProps>(MyComponent, {
      title: this.properties.title,
      context: this.context,
    });
    ReactDom.render(element, this.domElement);
  }

  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          groups: [
            {
              groupFields: [
                PropertyPaneTextField('title', { label: 'Title' }),
              ],
            },
          ],
        },
      ],
    };
  }

  protected get dataVersion(): Version {
    return Version.parse('1.0');
  }
}

πŸ—οΈ Application Customizer

An application customizer runs on every page load and can inject UI into two predefined placeholders: PageHeader and PageFooter. It has no property pane β€” configuration is passed via clientSideComponentProperties at deployment time.

Use an application customizer when:

  • You need something that appears on every page without editor intervention
  • You're building a global navigation bar, site-wide banner, or persistent footer
  • The UI must be consistent across all pages and cannot be removed by editors
import { override } from '@microsoft/decorators';
import { BaseApplicationCustomizer, PlaceholderContent, PlaceholderName } from '@microsoft/sp-application-base';
import * as React from 'react';
import * as ReactDom from 'react-dom';
import GlobalHeader from './components/GlobalHeader';

export interface IGlobalNavApplicationCustomizerProperties {
  logoUrl: string;
}

export default class GlobalNavApplicationCustomizer
  extends BaseApplicationCustomizer<IGlobalNavApplicationCustomizerProperties> {

  private _headerPlaceholder: PlaceholderContent | undefined;

  @override
  public onInit(): Promise<void> {
    // Re-render when placeholders change (page navigation in SPAs)
    this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceholders);
    this._renderPlaceholders();
    return Promise.resolve();
  }

  private _renderPlaceholders(): void {
    if (!this._headerPlaceholder) {
      this._headerPlaceholder = this.context.placeholderProvider.tryCreateContent(
        PlaceholderName.Top,
        { onDispose: this._onDispose }
      );
    }

    if (this._headerPlaceholder?.domElement) {
      const element = React.createElement(GlobalHeader, {
        logoUrl: this.properties.logoUrl,
        context: this.context,
      });
      ReactDom.render(element, this._headerPlaceholder.domElement);
    }
  }

  private _onDispose(): void {
    ReactDom.unmountComponentAtNode(this._headerPlaceholder?.domElement as Element);
  }
}

🎨 Field Customizer

A field customizer overrides how a specific list column cell renders in list views. It receives the raw field value and lets you output any HTML/React in its place.

Use a field customizer when:

  • You need custom column rendering β€” status badges, progress bars, formatted dates
  • JSON column formatting isn't powerful enough (you need API calls or complex logic)
  • The rendering logic needs TypeScript, not just JSON templates
import { Log } from '@microsoft/sp-core-library';
import { BaseFieldCustomizer, IFieldCustomizerCellEventParameters } from '@microsoft/sp-listview-extensibility';
import * as React from 'react';
import * as ReactDom from 'react-dom';
import StatusBadge from './components/StatusBadge';

export default class StatusFieldCustomizer
  extends BaseFieldCustomizer<{}> {

  public onRenderCell(event: IFieldCustomizerCellEventParameters): void {
    const statusValue: string = event.fieldValue as string;

    const element = React.createElement(StatusBadge, {
      status: statusValue,
    });

    ReactDom.render(element, event.domElement);
  }

  public onDisposeCell(event: IFieldCustomizerCellEventParameters): void {
    ReactDom.unmountComponentAtNode(event.domElement);
  }
}

The StatusBadge component receives the raw column value and renders a color-coded badge β€” something JSON formatting simply cannot do when the color logic depends on an external data source.


⚑ Command Set

A command set adds custom buttons to the list toolbar (top commands) or the item context menu (right-click / ellipsis). It receives selection state so you can enable/disable buttons based on what's selected.

Use a command set when:

  • You need a custom action on selected list items β€” bulk approve, export, move
  • You want to extend the list toolbar without modifying the list itself
  • The action needs access to item IDs and SharePoint context
import { override } from '@microsoft/decorators';
import { BaseListViewCommandSet, Command, IListViewCommandSetExecuteEventParameters, IListViewCommandSetListViewUpdatedParameters } from '@microsoft/sp-listview-extensibility';

export default class ApproveCommandSet
  extends BaseListViewCommandSet<{}> {

  @override
  public onInit(): Promise<void> {
    return Promise.resolve();
  }

  @override
  public onListViewUpdated(event: IListViewCommandSetListViewUpdatedParameters): void {
    const approveCommand: Command = this.tryGetCommand('APPROVE_ITEMS');

    if (approveCommand) {
      // Only enable the button when at least one item is selected
      approveCommand.visible = event.selectedRows.length > 0;
    }
  }

  @override
  public onExecute(event: IListViewCommandSetExecuteEventParameters): void {
    if (event.itemId === 'APPROVE_ITEMS') {
      const selectedIds = event.selectedRows.map(r => r.getValueByName('ID'));
      // Trigger your approval logic here
      console.log('Approving items:', selectedIds);
    }
  }
}

πŸ”‘ Decision Guide β€” Which One Do You Need?

Answer these three questions:

1. Does it need to appear on every page automatically?
β†’ Yes β†’ Application Customizer

2. Does it replace how a list column renders?
β†’ Yes β†’ Field Customizer

3. Does it add an action button to a list toolbar or item menu?
β†’ Yes β†’ Command Set

Everything else (canvas-based UI, configurable components, page layouts)
β†’ Web Part


πŸš€ Deploy the Solution

All four extension types are packaged the same way:

npm run build
npm run bundle -- --ship
npm run package-solution -- --ship

Upload the .sppkg to your App Catalog. Web parts are added by editors via the web part picker. Extensions (Application Customizer, Field Customizer, Command Set) are activated via site-level app deployment, tenant-wide deployment, or site scripts.


πŸ“‚ GitHub Source

View full SPFx project on GitHub:SPFx Extension Types β€” Web Part, Application Customizer, Field Customizer, Command Set

GitHub

βœ… Summary

  • Web Part β€” page canvas, editor-controlled, property pane configuration
  • Application Customizer β€” runs on every page, header/footer placeholders, no editor control
  • Field Customizer β€” replaces list column cell rendering, receives raw field value
  • Command Set β€” adds toolbar/context menu buttons to list views, selection-aware
  • When JSON column formatting is insufficient, reach for a Field Customizer
  • When you need a site-wide UI injection, Application Customizer is the only correct choice

Happy coding!

Ad image