- 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:
| Method | When it fires | What 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 DOM | Unmount the React component |
The event object gives you:
event.fieldValue— the raw column value as a stringevent.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 fromevent.fieldValueorevent.listItem. If you need supplementary data, load it once inonInitand 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
FluentProviderper cell adds overhead. Consider rendering it once inonInitinto 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
✅ Summary
onRenderCellfires for every visible row — keep it synchronous, fast, and side-effect-free.onDisposeCellmust unmount the React tree — missing this causes memory leaks as the user scrolls.- Register the field customizer on a column by setting
ClientSideComponentIdon the field object, not by adding it to a page. - Use
event.fieldValuefor the column value andevent.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!
Author
Ravichandran@Hi_Ravichandran
