Custom Fields in SharePoint Framework (SPFx): Provisioning, Rendering, Editing, and Property Pane Controls (with PnP Examples)
When you say “custom fields in SPFx”, you usually mean all of these:
- Provision fields (columns) in a list/library (and optionally content types)
- Render fields differently in list views (Field Customizer)
- Edit fields with a custom New/Edit/View form (Form Customizer)
- Create custom “fields” in the Property Pane (web part configuration UI)
Below is a single, publish-ready technical article with practical examples for each, plus PnP links you can learn from.
0) Quick decision rule (before coding)
If your goal is only “make it look nicer” (colors, icons, small layouts), consider SharePoint Column Formatting JSON.
If you need:
- a real component,
- async calls,
- complex UI/behavior,
- deeper logic,
then SPFx Extensions (Field Customizer / Form Customizer) are the right tools. Microsoft positions SPFx Extensions exactly for these “extend the experience” use cases. (Microsoft Learn)
1) Provisioning custom fields (columns) — with SPHttpClient and with PnPjs
1.1 Provision a column using SPHttpClient (REST)
This is useful when your web part works like a “setup wizard” (create list + fields + defaults).
import { SPHttpClient } from "@microsoft/sp-http";export async function ensureTextField( spHttpClient: SPHttpClient, webUrl: string, listTitle: string, internalName: string, displayName: string): Promise<void> { const endpoint = `${webUrl}/_api/web/lists/getbytitle('${encodeURIComponent(listTitle)}')/fields`; const body = JSON.stringify({ "__metadata": { "type": "SP.FieldText" }, "Title": displayName, "InternalName": internalName, "Required": false, "MaxLength": 255 }); const resp = await spHttpClient.post(endpoint, SPHttpClient.configurations.v1, { headers: { "Accept": "application/json;odata=nometadata", "Content-Type": "application/json;odata=nometadata" }, body }); if (!resp.ok) { const txt = await resp.text(); throw new Error(`Field creation failed. Status=${resp.status}. Body=${txt}`); }}
Why this matters
- You keep full control (no extra libs).
- You must be precise with field types and payloads.
1.2 Provision a column using PnPjs (cleaner, more readable)
PnPjs makes SharePoint operations much more fluent.
Microsoft Learn (PnPjs in SPFx): (Microsoft Learn)
PnPjs Getting Started: (pnp.github.io)
Step A — Install PnPjs
npm i @pnp/sp @pnp/logging @pnp/queryable
Step B — Configure PnPjs in your WebPart onInit()
import { spfi, SPFI } from "@pnp/sp";import { SPFx } from "@pnp/spfx";export default class MyWebPart /* ... */ { private sp: SPFI; protected async onInit(): Promise<void> { await super.onInit(); this.sp = spfi().using(SPFx(this.context)); }}
Step C — Create a text field in a list with PnPjs
import "@pnp/sp/webs";import "@pnp/sp/lists";import "@pnp/sp/fields";export async function ensureTextFieldPnP( sp: import("@pnp/sp").SPFI, listTitle: string, internalName: string, displayName: string): Promise<void> { const list = sp.web.lists.getByTitle(listTitle); // Basic example: create a text field await list.fields.addText(displayName, 255, { Group: "Custom Columns", StaticName: internalName });}
What you learn here
- SPHttpClient teaches you “raw REST”
- PnPjs teaches you “developer ergonomics” and speeds up real projects (pnp.github.io)
2) Custom rendering in list views — Field Customizer (SPFx Extension)
A Field Customizer changes how a field appears in list/library views (not in forms). Microsoft Learn’s official tutorial is the reference path. (Microsoft Learn)
Example: Render a “Status” field as a colored pill badge
Step 1 — Create the extension
yo @microsoft/sharepoint# Choose: Extension -> Field Customizer -> (React or no framework)
Step 2 — Implement a simple renderer
import { BaseFieldCustomizer } from "@microsoft/sp-listview-extensibility";export default class StatusBadgeFieldCustomizer extends BaseFieldCustomizer<{}> { public onRenderCell(event: any): void { const value: string = (event.fieldValue || "").toString(); const bg = value === "Approved" ? "#107C10" : value === "Rejected" ? "#A4262C" : value === "Pending" ? "#986F0B" : "#605E5C"; event.domElement.innerHTML = ` <span style=" display:inline-block; padding:2px 10px; border-radius:999px; font-size:12px; color:#fff; background:${bg}; ">${escapeHtml(value)}</span>`; } public onDisposeCell(event: any): void { event.domElement.innerHTML = ""; }}function escapeHtml(input: string): string { return input .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'");}
Step 3 — Associate the customizer to a field
This is the part that people miss: your extension exists, but it must be associated (site/list field level). Microsoft Learn explains association options in the tutorial flow. (Microsoft Learn)
3) Custom editing experience — Form Customizer (SPFx Extension)
A Form Customizer replaces the New/Edit/View experience for a list or content type. Microsoft Learn has the end-to-end tutorial. (Microsoft Learn)
Example: A custom “New Item” form with validation + save
Step 1 — Create the extension
yo @microsoft/sharepoint# Choose: Extension -> Form Customizer -> React
Step 2 — Implement the React form skeleton
import * as React from "react";import { PrimaryButton, TextField, MessageBar } from "@fluentui/react";import { SPHttpClient } from "@microsoft/sp-http";export function CustomNewForm(props: { spHttpClient: SPHttpClient; webUrl: string; listTitle: string; onSaved: () => void;}) { const [title, setTitle] = React.useState(""); const [error, setError] = React.useState<string | null>(null); const [saving, setSaving] = React.useState(false); const save = async () => { setError(null); if (!title.trim()) { setError("Title is required."); return; } setSaving(true); try { const endpoint = `${props.webUrl}/_api/web/lists/getbytitle('${encodeURIComponent(props.listTitle)}')/items`; const body = JSON.stringify({ Title: title }); const resp = await props.spHttpClient.post(endpoint, SPHttpClient.configurations.v1, { headers: { "Accept": "application/json;odata=nometadata", "Content-Type": "application/json;odata=nometadata" }, body }); if (!resp.ok) { const txt = await resp.text(); throw new Error(txt); } props.onSaved(); } catch (e: any) { setError(e?.message || "Unknown error saving item."); } finally { setSaving(false); } }; return ( <div style={{ padding: 16 }}> <h2>Custom New Item</h2> {error && <MessageBar messageBarType={1}>{error}</MessageBar>} <TextField label="Title" value={title} onChange={(_, v) => setTitle(v || "")} /> <PrimaryButton text={saving ? "Saving..." : "Save"} onClick={save} disabled={saving} style={{ marginTop: 12 }} /> </div> );}
Step 3 — Associate the Form Customizer to a content type / list
This is mandatory: it won’t take over forms until you associate it. The Learn tutorial shows the correct approach. (Microsoft Learn)
4) “Custom fields” in the Web Part Property Pane — use PnP Property Controls
If your goal is:
“I want advanced property pane inputs (people picker, list picker, taxonomy picker…)”
Then PnP SPFx Property Controls is the standard approach:
- Official docs site: (pnp.github.io)
- GitHub repo: (GitHub)
- Example: People Picker documentation: (GitHub)
Example: People Picker in property pane
Step 1 — Install
npm i @pnp/spfx-property-controls
Step 2 — Use it in your web part properties
import { IPropertyPaneConfiguration } from "@microsoft/sp-property-pane";import { PropertyFieldPeoplePicker, PrincipalType} from "@pnp/spfx-property-controls/lib/PropertyFieldPeoplePicker";export interface IMyWebPartProps { approvers: any[];}protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { return { pages: [ { header: { description: "Configuration" }, groups: [ { groupName: "Approval", groupFields: [ PropertyFieldPeoplePicker("approvers", { label: "Approvers", initialData: this.properties.approvers, allowDuplicate: false, principalType: [PrincipalType.User], onPropertyChange: this.onPropertyPaneFieldChanged.bind(this), context: this.context, properties: this.properties, key: "approversPicker" }) ] } ] } ] };}
Why this is “custom fields”
- You’re building a strong configuration layer without hand-rolling complex property pane UI.
- This is one of the fastest “learn-by-doing” wins for SPFx.
5) Recommended learning path (how to practice all of it)
- Start with PnPjs in a Web Part: read + implement
spfi().using(SPFx(context))and do basic list/field operations (Microsoft Learn) - Build a Field Customizer and render a field as a badge (fast feedback in list views) (Microsoft Learn)
- Build a Form Customizer and replace New/Edit (learn validation + submit patterns) (Microsoft Learn)
- Add PnP Property Controls to your web part (PeoplePicker/ListPicker/TermPicker) (pnp.github.io)
Final summary tables
Table 1 — What to use for each “custom field” scenario
| Scenario | Best Tool | Where it runs | Example from article |
|---|---|---|---|
| Create/provision columns | SPHttpClient or PnPjs | Web part / setup logic | ensureTextField / ensureTextFieldPnP |
| Custom display in views | Field Customizer | List/library view | Status badge pill |
| Custom New/Edit/View form | Form Customizer | List forms | Custom React New Form |
| Advanced property pane inputs | PnP Property Controls | Web part configuration | PeoplePicker |
Table 2 — Key “learn-by-doing” references (Microsoft Learn + PnP)
| Topic | Source |
|---|---|
| Field Customizer tutorial | (Microsoft Learn) |
| Form Customizer tutorial | (Microsoft Learn) |
| SPFx Extensions overview | (Microsoft Learn) |
| Using PnPjs in SPFx (Microsoft Learn) | (Microsoft Learn) |
| PnPjs Getting Started | (pnp.github.io) |
| PnP SPFx Property Controls docs | (pnp.github.io) |
| PnP Property Controls GitHub | (GitHub) |
| PeoplePicker control docs | (GitHub) |
