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:

  1. Provision fields (columns) in a list/library (and optionally content types)
  2. Render fields differently in list views (Field Customizer)
  3. Edit fields with a custom New/Edit/View form (Form Customizer)
  4. 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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}

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:

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)

  1. Start with PnPjs in a Web Part: read + implement spfi().using(SPFx(context)) and do basic list/field operations (Microsoft Learn)
  2. Build a Field Customizer and render a field as a badge (fast feedback in list views) (Microsoft Learn)
  3. Build a Form Customizer and replace New/Edit (learn validation + submit patterns) (Microsoft Learn)
  4. 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

ScenarioBest ToolWhere it runsExample from article
Create/provision columnsSPHttpClient or PnPjsWeb part / setup logicensureTextField / ensureTextFieldPnP
Custom display in viewsField CustomizerList/library viewStatus badge pill
Custom New/Edit/View formForm CustomizerList formsCustom React New Form
Advanced property pane inputsPnP Property ControlsWeb part configurationPeoplePicker

Table 2 — Key “learn-by-doing” references (Microsoft Learn + PnP)

TopicSource
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)

Edvaldo Guimrães Filho Avatar

Published by