- Published on
JSON Column Formatting vs SPFx Field Customizer — Decision Guide
Whenever a SharePoint developer needs to change how a list column renders in a view, they face the same choice: JSON column formatting or an SPFx field customizer.
Both can produce coloured badges, icons, and clickable elements. Both run inside the list view cell. But they have fundamentally different deployment models, capability ceilings, and maintenance costs — and picking the wrong one creates work you will regret later.
This is the decision guide I wish I had when I started with SPFx.
🗺️ What Each One Is
JSON column formatting is a declarative JSON configuration applied directly to a list column through the SharePoint UI. No code, no deployment, no App Catalog. A power user with "Manage Lists" permission can apply it by pasting JSON into the column settings.
SPFx field customizer is a TypeScript/React component deployed through the SPFx pipeline — scaffolded with Yeoman, bundled with webpack, packaged as an .sppkg, deployed through the App Catalog, and registered on a list column via PowerShell or PnPjs. Full React component, full TypeScript, full access to any npm package.
✅ Capability Comparison
| Capability | JSON Formatting | SPFx Field Customizer |
|---|---|---|
| Colour coding based on value | ✅ | ✅ |
| Icons from Fluent UI icon set | ✅ (via iconName) | ✅ |
| Custom SVG icons | ❌ | ✅ |
| Clickable links and buttons | ✅ | ✅ |
| Tooltips | ✅ (limited) | ✅ (full Fluent UI) |
| Conditional visibility of elements | ✅ (@currentField comparisons) | ✅ |
| Complex conditional logic | ⚠️ Limited operator set | ✅ Full TypeScript |
| External data fetch on render | ❌ | ✅ |
| State and interactivity (expand/collapse, modals) | ❌ | ✅ |
| Animations | ❌ | ✅ |
| Access to other columns in the row | ✅ ([$OtherColumn]) | ✅ (event.listItem) |
| Access to current user | ✅ (@me) | ✅ |
| Access to today's date | ✅ (@now) | ✅ |
| Deployment required | ❌ | ✅ App Catalog |
| Applied per column | ✅ Pasted in UI | ✅ Via PowerShell or PnPjs |
| Editable by power users | ✅ | ❌ Requires developer |
| Performance overhead | Very low | Low–medium (React mount per cell) |
🎯 The Decision Rule
Default to JSON column formatting. It covers approximately 80% of real-world column customisation requirements. No deployment pipeline, no App Catalog, no developer needed to update a colour or swap an icon.
Reach for a field customizer when:
- You need to fetch external data per row (API call, Graph, another list)
- You need interactive state — an expand/collapse toggle, an inline edit, a popover that fetches data on open
- Your conditional logic is too complex for the JSON operator set (nested conditions, arithmetic, regex)
- You need custom SVG icons, animations, or third-party component libraries
- The column rendering has business logic that must be type-safe and unit-tested
🧩 JSON Formatting — What It Can Do
A status badge with colour coding — the most common use case:
{
"$schema": "https://developer.microsoft.com/json-schemas/sp/v2/column-formatting.schema.json",
"elmType": "div",
"style": {
"display": "flex",
"align-items": "center",
"gap": "6px"
},
"children": [
{
"elmType": "span",
"style": {
"background-color": "=if(@currentField == 'Completed', '#107C10', if(@currentField == 'Blocked', '#D13438', if(@currentField == 'In Progress', '#0078D4', '#8A8886')))",
"color": "#ffffff",
"padding": "2px 8px",
"border-radius": "4px",
"font-size": "12px",
"font-weight": "600"
},
"txtContent": "@currentField"
}
]
}
A row-level action button that opens a form:
{
"$schema": "https://developer.microsoft.com/json-schemas/sp/v2/column-formatting.schema.json",
"elmType": "button",
"customRowAction": {
"action": "executeFlow",
"actionParams": "{\"id\": \"your-flow-id\"}"
},
"style": {
"background": "#0078D4",
"color": "#ffffff",
"border": "none",
"padding": "4px 12px",
"border-radius": "4px",
"cursor": "pointer"
},
"txtContent": "Submit for Approval"
}
JSON formatting can call Power Automate flows, navigate to URLs, open panels, and display context menus — all without a single line of TypeScript.
🧩 Field Customizer — When JSON Falls Short
A rating stars column that fetches the average rating from a separate list on cell render:
// This is impossible in JSON formatting — requires async data fetch
const RatingCell: React.FC<{ itemId: string; sp: SPFI }> = ({ itemId, sp }) => {
const [avg, setAvg] = React.useState<number | null>(null);
React.useEffect(() => {
sp.web.lists
.getByTitle('Reviews')
.items
.filter(`ProjectId eq ${itemId}`)
.select('Rating')()
.then(items => {
const ratings = items.map(i => i.Rating);
setAvg(ratings.length ? ratings.reduce((s, r) => s + r, 0) / ratings.length : null);
});
}, [itemId]);
if (avg === null) return <span>—</span>;
return (
<span>{'★'.repeat(Math.round(avg))}{'☆'.repeat(5 - Math.round(avg))} ({avg.toFixed(1)})</span>
);
};
An interactive inline toggle that updates a field without leaving the list view:
// Inline toggle — impossible in JSON formatting (requires React state + PnPjs write)
const StatusToggle: React.FC<{ itemId: number; status: string; sp: SPFI }> = ({ itemId, status, sp }) => {
const [current, setCurrent] = React.useState(status);
const [saving, setSaving] = React.useState(false);
const toggle = async () => {
const next = current === 'Completed' ? 'In Progress' : 'Completed';
setSaving(true);
await sp.web.lists.getByTitle('Tasks').items.getById(itemId).update({ Status: next });
setCurrent(next);
setSaving(false);
};
return (
<Switch
checked={current === 'Completed'}
onChange={toggle}
disabled={saving}
label={current}
/>
);
};
Neither of these is achievable in JSON formatting. Both require a field customizer.
🤔 What About Maintenance?
JSON formatting is maintained in the column settings UI. Any site owner with "Manage Lists" can edit it. When business rules change — a new status value, a new colour — a power user updates the JSON, no developer engagement required.
Field customizer requires a developer to update the TypeScript, rebuild, repackage, and redeploy the .sppkg. This is the correct tradeoff when the logic genuinely requires code — but it is the wrong tradeoff when a colour change ends up as a Jira ticket, a sprint slot, and a deployment window.
The real maintenance cost is not the update itself — it is the process around it. JSON formatting skips the entire process.
✅ Summary
- Start with JSON column formatting. It is faster to build, easier to maintain, and covers 80% of real-world requirements.
- Upgrade to a field customizer when you need async data, interactive state, complex logic, or custom components that JSON cannot express.
- The deployment cost of a field customizer is real — factor it into your decision. A colour change that requires a sprint slot is a sign you over-engineered the solution.
- Both tools coexist — you can have JSON formatting on one column and a field customizer on another in the same list view.
- When in doubt: build it in JSON first. You can always migrate to a field customizer later if requirements grow beyond what JSON supports.
Happy coding!
Author
Ravichandran@Hi_Ravichandran
