logo
Published on

SPFx Field Customizer — Transform Any List Column

A field customizer replaces the default cell renderer for a specific list column with any React component you want. Instead of plain text like "In Progress", you can render a coloured badge with an icon. Instead of a date string, you can show "3 days ago". Instead of a URL, you can render a clickable button.
The field customizer fires for every row in list view — it needs to be fast, lightweight, and free of side effects. This article builds a practical status badge customizer from scaffold to deployment.


📦 Scaffold the Extension

yo @microsoft/sharepoint

When prompted:

  • What type of client-side component? → Extension
  • Which type of client-side extension? → Field Customizer
  • What is your Field Customizer name?StatusBadge
  • Framework? → React

⚙️ The Field Customizer Lifecycle

Unlike web parts or application customizers, a field customizer does not have a persistent React root. It uses two lifecycle methods:

MethodWhen it firesWhat to do
onRenderCell(event)Every time a cell needs to render (initial load + re-renders)Mount the React component into event.domElement
onDisposeCell(event)When the cell is removed from the DOMUnmount the React component

The event object gives you:

  • event.fieldValue — the raw column value as a string
  • event.listItem — the full list item row (all fields loaded in the view)
  • event.domElement — the container <div> SharePoint provides for your render

🧩 Field Customizer Class

src/extensions/statusBadge/StatusBadgeFieldCustomizer.ts

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 { FluentProvider, webLightTheme } from '@fluentui/react-components';
import StatusBadgeCell from './components/StatusBadgeCell';

export interface IStatusBadgeProperties {
  // Optional: future extensibility — colour mapping could come from properties
}

export default class StatusBadgeFieldCustomizer
  extends BaseFieldCustomizer<IStatusBadgeProperties> {

  public onRenderCell(event: IFieldCustomizerCellEventParameters): void {
    const value = event.fieldValue;
    const itemId = event.listItem.getValueByName('ID') as string;

    ReactDOM.render(
      React.createElement(
        FluentProvider,
        { theme: webLightTheme },
        React.createElement(StatusBadgeCell, {
          status: value,
          itemId
        })
      ),
      event.domElement
    );
  }

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

🧩 Status Badge Cell Component

src/extensions/statusBadge/components/StatusBadgeCell.tsx

import * as React from 'react';
import {
  Badge,
  Tooltip,
  makeStyles,
  tokens
} from '@fluentui/react-components';
import {
  CheckmarkCircleRegular,
  ClockRegular,
  ArrowCircleRightRegular,
  DismissCircleRegular,
  QuestionCircleRegular
} from '@fluentui/react-icons';

type BadgeColor = 'success' | 'warning' | 'danger' | 'informative' | 'subtle';

interface IStatusConfig {
  color: BadgeColor;
  icon: React.ReactElement;
  tooltip: string;
}

const STATUS_MAP: Record<string, IStatusConfig> = {
  'Completed': {
    color: 'success',
    icon: React.createElement(CheckmarkCircleRegular),
    tooltip: 'This item has been completed'
  },
  'In Progress': {
    color: 'informative',
    icon: React.createElement(ArrowCircleRightRegular),
    tooltip: 'Work is currently in progress'
  },
  'Not Started': {
    color: 'subtle',
    icon: React.createElement(ClockRegular),
    tooltip: 'Work has not yet started'
  },
  'Blocked': {
    color: 'danger',
    icon: React.createElement(DismissCircleRegular),
    tooltip: 'This item is blocked and needs attention'
  }
};

const DEFAULT_STATUS: IStatusConfig = {
  color: 'subtle',
  icon: React.createElement(QuestionCircleRegular),
  tooltip: 'Status unknown'
};

const useStyles = makeStyles({
  cell: {
    display: 'flex',
    alignItems: 'center',
    height: '100%'
  }
});

interface IStatusBadgeCellProps {
  status: string;
  itemId: string;
}

const StatusBadgeCell: React.FC<IStatusBadgeCellProps> = ({ status }) => {
  const styles = useStyles();
  const config = STATUS_MAP[status] ?? DEFAULT_STATUS;

  return (
    <div className={styles.cell}>
      <Tooltip content={config.tooltip} relationship="label">
        <Badge
          color={config.color}
          icon={config.icon}
          appearance="filled"
        >
          {status || 'Unknown'}
        </Badge>
      </Tooltip>
    </div>
  );
};

export default StatusBadgeCell;

⚙️ Registering the Field Customizer on a Column

The field customizer must be registered on a specific list column. The easiest way during development is via the serve.json fieldCustomizers configuration:

config/serve.json

{
  "serveConfigurations": {
    "default": {
      "pageUrl": "https://contoso.sharepoint.com/sites/dev/Lists/Projects/AllItems.aspx",
      "fieldCustomizers": {
        "Status": {
          "id": "your-component-id-from-manifest.json",
          "properties": {}
        }
      }
    }
  }
}

The key "Status" is the internal name of the column you want to customise. The field customizer only runs for that specific column on this list.


🚀 Register on a Real Column via PnPjs

For production deployment, register the field customizer on the column:

import { spfi, SPFx } from '@pnp/sp';
import '@pnp/sp/webs';
import '@pnp/sp/lists';
import '@pnp/sp/fields';

const sp = spfi().using(SPFx(context));

// Register the field customizer on the Status column
await sp.web.lists
  .getByTitle('Projects')
  .fields
  .getByInternalNameOrTitle('Status')
  .update({
    ClientSideComponentId: 'your-component-id-from-manifest.json',
    ClientSideComponentProperties: '{}'
  });

Or via PnP PowerShell:

Set-PnPField -List "Projects" -Identity "Status" -Values @{
  ClientSideComponentId = "your-component-id-from-manifest.json"
  ClientSideComponentProperties = "{}"
}

⚠️ Performance Rules for Field Customizers

Field customizers run in every visible row. A list view with 100 rows fires onRenderCell 100 times. Keep your component fast:

  • No async calls in onRenderCell — do not fetch data inside the cell render. All data should come from event.fieldValue or event.listItem. If you need supplementary data, load it once in onInit and store it on the class.
  • No heavy computation — the badge color and icon lookup should be a constant map, not a function that runs a loop.
  • Keep the FluentProvider shallow — rendering FluentProvider per cell adds overhead. Consider rendering it once in onInit into a wrapper and reusing the context. For simple badges this is acceptable; for complex components it matters.
  • Minimal DOM — the cell container is small. Keep your component's output lean.

🚀 Deploy

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

Upload the .sppkg to your App Catalog, deploy it, then register the field customizer on the target column via PnPjs or PowerShell.


📂 GitHub Source

View full SPFx project on GitHub:SPFx field customizer — status badge with color coding, icons and tooltip

GitHub

✅ Summary

  • onRenderCell fires for every visible row — keep it synchronous, fast, and side-effect-free.
  • onDisposeCell must unmount the React tree — missing this causes memory leaks as the user scrolls.
  • Register the field customizer on a column by setting ClientSideComponentId on the field object, not by adding it to a page.
  • Use event.fieldValue for the column value and event.listItem.getValueByName() for other fields in the same row.
  • Before reaching for a field customizer, consider JSON column formatting — it handles most colour-coding and icon scenarios without a deployment pipeline.

Happy coding!

Ad image