Using the PnP PropertyFieldPeoplePicker Control in an SPFx Web Part

The PropertyFieldPeoplePicker control from the PnP SPFx Property Controls library adds a SharePoint-aware people and group picker to the property pane of a SharePoint Framework web part.

It can search for and select:

  • Microsoft 365 and SharePoint users
  • SharePoint groups
  • Security groups
  • Multiple users or groups
  • Users and groups from another SharePoint site, when targetSiteUrl is configured

Unlike a standard text field, the control resolves SharePoint principals and stores structured information about every selected user or group.

Official documentation:

PropertyFieldPeoplePicker – PnP SPFx Property Controls

The official PnP documentation defines this control as a people and group picker designed specifically for an SPFx web part property pane.


Installing the PnP Property Controls Package

The control is distributed through the @pnp/spfx-property-controls package.

Run the following command from the root directory of the existing SPFx solution:

npm install @pnp/spfx-property-controls --save

After installing the package, restart the local development process if it is already running:

heft start

Required Imports

The control, the principal type enumeration, and the selected-person interface are exported from the same package path:

import {
PropertyFieldPeoplePicker,
PrincipalType,
IPropertyFieldGroupOrPerson
} from '@pnp/spfx-property-controls/lib/PropertyFieldPeoplePicker';

The imports may also be separated:

import {
PropertyFieldPeoplePicker,
PrincipalType
} from '@pnp/spfx-property-controls/lib/PropertyFieldPeoplePicker';
import {
IPropertyFieldGroupOrPerson
} from '@pnp/spfx-property-controls/lib/PropertyFieldPeoplePicker';

Both approaches are valid, although the combined import is more concise.

The official example uses PropertyFieldPeoplePicker, PrincipalType, and IPropertyFieldGroupOrPerson from this package path.


Defining the Web Part Property

The selected people and groups must be stored in the web part properties.

export interface IPropertyFieldPeoplePickerWpWebPartProps {
description: string;
people: IPropertyFieldGroupOrPerson[];
}

The people property is an array because the control supports multiple selections by default.

Each selected entry is represented by an IPropertyFieldGroupOrPerson object.

The interface can contain information such as:

{
id: string;
fullName: string;
login: string;
email: string;
jobTitle: string;
initials: string;
imageUrl: string;
description: string;
}

Not every property is guaranteed to be populated. The available information depends on whether the selected principal is a user, SharePoint group, or security group.

According to the PnP interface documentation, fullName and login are the primary required values, while fields such as email, jobTitle, initials, and imageUrl are optional.


Important Correction in onInit()

The original implementation contains the following code:

protected onInit(): Promise<void> {
this.properties.people = [];
return this._getEnvironmentMessage().then(message => {
this._environmentMessage = message;
});
}

This creates a persistence problem.

Every time the web part initializes, the following line replaces the stored selection with an empty array:

this.properties.people = [];

Consequently, users or groups previously selected and saved in the web part properties may disappear when the page loads again.

Instead, initialize the property only when it does not already contain a value:

protected onInit(): Promise<void> {
if (!this.properties.people) {
this.properties.people = [];
}
return this._getEnvironmentMessage().then(message => {
this._environmentMessage = message;
});
}

A shorter equivalent is:

protected onInit(): Promise<void> {
this.properties.people = this.properties.people || [];
return this._getEnvironmentMessage().then(message => {
this._environmentMessage = message;
});
}

The first version is usually clearer because it explicitly demonstrates that initialization occurs only when the property is undefined.


Configuring the PropertyFieldPeoplePicker

The control is added to the groupFields array of the property pane configuration:

PropertyFieldPeoplePicker('people', {
label: 'PropertyFieldPeoplePicker',
initialData: this.properties.people,
allowDuplicate: false,
principalType: [
PrincipalType.Users,
PrincipalType.SharePoint,
PrincipalType.Security
],
onPropertyChange: this.onPropertyPaneFieldChanged,
context: this.context as any,
properties: this.properties,
deferredValidationTime: 0,
key: 'peopleFieldId',
searchTextLimit: 3
})

The first argument is:

'people'

This value must match the property declared in the web part properties interface:

people: IPropertyFieldGroupOrPerson[];

The picker writes the selected values to:

this.properties.people

Understanding the Control Properties

label

label: 'PropertyFieldPeoplePicker'

Defines the label displayed above the picker in the property pane.

A more descriptive label could be used in a real solution:

label: 'Select users or groups'

initialData

initialData: this.properties.people

Loads the existing web part property value into the picker.

This is what allows previously selected users and groups to appear again when the property pane is reopened.

The value must be an array of:

IPropertyFieldGroupOrPerson[]

The official documentation identifies initialData as the optional initial collection loaded by the people picker.


allowDuplicate

allowDuplicate: false

Prevents the same user or group from being selected more than once.

For most web part configurations, duplicate principals do not provide useful information, so false is generally the recommended value.

To allow duplicates:

allowDuplicate: true

principalType

principalType: [
PrincipalType.Users,
PrincipalType.SharePoint,
PrincipalType.Security
]

Determines which principal types can be returned by the search.

The available values are:

PrincipalType.Users
PrincipalType.SharePoint
PrincipalType.Security

Their meanings are:

Principal typePurpose
PrincipalType.UsersSearches for users
PrincipalType.SharePointSearches for SharePoint groups
PrincipalType.SecuritySearches for security groups

The control accepts an array, so more than one principal type can be enabled.

The official PnP documentation confirms that users, SharePoint groups, and security groups can be enabled independently or together.

Users only

principalType: [
PrincipalType.Users
]

Users and SharePoint groups

principalType: [
PrincipalType.Users,
PrincipalType.SharePoint
]

Groups only

principalType: [
PrincipalType.SharePoint,
PrincipalType.Security
]

All supported principal types

principalType: [
PrincipalType.Users,
PrincipalType.SharePoint,
PrincipalType.Security
]

onPropertyChange

onPropertyChange: this.onPropertyPaneFieldChanged

Connects the custom control to the standard SPFx property pane property-change mechanism.

When the selection changes, the picker uses this callback to update the corresponding web part property.

Because the control name is people, the result is stored in:

this.properties.people

context

context: this.context as any

Provides the SPFx context required by the control to communicate with SharePoint and resolve users and groups.

The official control contract expects a BaseComponentContext.

In many compatible SPFx and package combinations, this can be passed directly:

context: this.context

However, some SPFx projects encounter a TypeScript compatibility error caused by duplicate or mismatched versions of Microsoft SPFx packages inside node_modules.

In that situation, developers sometimes use:

context: this.context as any

This bypasses the TypeScript incompatibility, but it should not be treated as the ideal architectural solution.

A better long-term approach is to verify the dependency tree:

npm list @microsoft/sp-component-base
npm list @microsoft/sp-module-interfaces
npm list @pnp/spfx-property-controls

If multiple incompatible SPFx package versions appear, clean and reinstall the dependencies:

Remove-Item -Path ".\node_modules" -Recurse -Force
Remove-Item -Path ".\package-lock.json" -Force
npm install

Use the cast only when the project has a known dependency compatibility problem and the actual runtime context is valid.


properties

properties: this.properties

Passes the parent web part properties object to the custom property control.

The picker uses this object together with onPropertyChange to update the selected value.

The official documentation marks this property as required.


deferredValidationTime

deferredValidationTime: 0

Defines how long the control waits before executing validation after the user stops typing.

The value is expressed in milliseconds.

With:

deferredValidationTime: 0

validation can occur immediately.

The official default value is 200 milliseconds.

Because the sample does not define onGetErrorMessage, this option has little practical effect in the current implementation.

A custom asynchronous validation method could look like this:

private _validatePeople(
value: IPropertyFieldGroupOrPerson[]
): string | Promise<string> {
if (!value || value.length === 0) {
return 'Select at least one user or group.';
}
return '';
}

It could then be connected with:

onGetErrorMessage: this._validatePeople.bind(this),
deferredValidationTime: 500

key

key: 'peopleFieldId'

Provides a unique identity for the property pane control.

Every custom property control should have a stable and unique key.

If two people pickers are added to the same property pane, they must use different keys:

key: 'ownersPeoplePicker'

and:

key: 'reviewersPeoplePicker'

searchTextLimit

searchTextLimit: 3

Defines the minimum number of characters that must be entered before the picker starts searching.

With this configuration:

searchTextLimit: 3

typing Jo will not start a search, but typing Joh can start one.

The official default value is also three characters.

A higher value may reduce unnecessary requests in large environments:

searchTextLimit: 4

A lower value can make searches begin sooner:

searchTextLimit: 2

Multiple and Single Selection

The control supports multiple selection by default.

The official documentation states that multiSelect is optional and defaults to true.

To make the configuration explicit:

multiSelect: true

To restrict the picker to one user or group:

multiSelect: false

Even when multiSelect is false, the property type remains an array:

people: IPropertyFieldGroupOrPerson[];

The array will normally contain zero or one entry.


Selecting Principals from Another Site

The optional targetSiteUrl property can define the SharePoint site from which users or groups should be retrieved.

Example:

targetSiteUrl: 'https://contoso.sharepoint.com/sites/hr'

This can be useful when the current web part runs on one site but must resolve SharePoint groups from another site.

The current sample does not use targetSiteUrl, so the picker operates using the current SPFx context.

The PnP documentation lists targetSiteUrl as an optional configuration property.


Passing the Selected Values to React

The web part passes the selected principals to the React component:

const element: React.ReactElement<IPropertyFieldPeoplePickerWpProps> =
React.createElement(
PropertyFieldPeoplePickerWp,
{
description: this.properties.description,
isDarkTheme: this._isDarkTheme,
environmentMessage: this._environmentMessage,
userDisplayName: this.context.pageContext.user.displayName,
people: this.properties.people
}
);

The important line is:

people: this.properties.people

This creates the following data flow:

Property pane People Picker
this.properties.people
React component props.people
Rendered users and groups

React Component Props Interface

The component interface can be defined as follows:

import {
IPropertyFieldGroupOrPerson
} from '@pnp/spfx-property-controls/lib/PropertyFieldPeoplePicker';
export interface IPropertyFieldPeoplePickerWpProps {
description: string;
isDarkTheme: boolean;
environmentMessage: string;
userDisplayName: string;
people: IPropertyFieldGroupOrPerson[];
}

The same interface used by the property pane is therefore also used by the React component.


Rendering the Selected Users and Groups

A practical React functional component can render the selected values as cards.

import * as React from 'react';
import {
IPropertyFieldPeoplePickerWpProps
} from './IPropertyFieldPeoplePickerWpProps';
const PropertyFieldPeoplePickerWp:
React.FC<IPropertyFieldPeoplePickerWpProps> = (props) => {
const people = props.people || [];
return (
<section>
<h2>PropertyFieldPeoplePicker</h2>
<p>{props.description}</p>
<p>
Current user: {props.userDisplayName}
</p>
<p>
Environment: {props.environmentMessage}
</p>
<p>
Dark theme: {props.isDarkTheme ? 'Yes' : 'No'}
</p>
<h3>Selected users and groups</h3>
{people.length === 0 ? (
<p>No users or groups have been selected.</p>
) : (
<ul>
{people.map((person, index) => (
<li key={person.login || person.id || index}>
<strong>{person.fullName}</strong>
{person.email && (
<div>Email: {person.email}</div>
)}
{person.login && (
<div>Login: {person.login}</div>
)}
{person.jobTitle && (
<div>Job title: {person.jobTitle}</div>
)}
</li>
))}
</ul>
)}
</section>
);
};
export default PropertyFieldPeoplePickerWp;

The defensive line:

const people = props.people || [];

prevents runtime errors if the property is undefined in an older web part instance.

Without this protection, the following expression could fail:

props.people.map(...)

Complete Corrected Web Part

import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
type IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import {
BaseClientSideWebPart
} from '@microsoft/sp-webpart-base';
import {
IReadonlyTheme
} from '@microsoft/sp-component-base';
import * as strings
from 'PropertyFieldPeoplePickerWpWebPartStrings';
import PropertyFieldPeoplePickerWp
from './components/PropertyFieldPeoplePickerWp';
import {
IPropertyFieldPeoplePickerWpProps
} from './components/IPropertyFieldPeoplePickerWpProps';
import {
PropertyFieldPeoplePicker,
PrincipalType,
IPropertyFieldGroupOrPerson
} from '@pnp/spfx-property-controls/lib/PropertyFieldPeoplePicker';
export interface IPropertyFieldPeoplePickerWpWebPartProps {
description: string;
people: IPropertyFieldGroupOrPerson[];
}
export default class PropertyFieldPeoplePickerWpWebPart
extends BaseClientSideWebPart<IPropertyFieldPeoplePickerWpWebPartProps> {
private _isDarkTheme: boolean = false;
private _environmentMessage: string = '';
public render(): void {
const element:
React.ReactElement<IPropertyFieldPeoplePickerWpProps> =
React.createElement(
PropertyFieldPeoplePickerWp,
{
description: this.properties.description,
isDarkTheme: this._isDarkTheme,
environmentMessage: this._environmentMessage,
userDisplayName:
this.context.pageContext.user.displayName,
people: this.properties.people || []
}
);
ReactDom.render(element, this.domElement);
}
protected onInit(): Promise<void> {
if (!this.properties.people) {
this.properties.people = [];
}
return this._getEnvironmentMessage()
.then((message: string) => {
this._environmentMessage = message;
});
}
private _getEnvironmentMessage(): Promise<string> {
if (this.context.sdks.microsoftTeams) {
return this.context.sdks.microsoftTeams.teamsJs.app
.getContext()
.then(context => {
let environmentMessage: string = '';
switch (context.app.host.name) {
case 'Office':
environmentMessage =
this.context.isServedFromLocalhost
? strings.AppLocalEnvironmentOffice
: strings.AppOfficeEnvironment;
break;
case 'Outlook':
environmentMessage =
this.context.isServedFromLocalhost
? strings.AppLocalEnvironmentOutlook
: strings.AppOutlookEnvironment;
break;
case 'Teams':
case 'TeamsModern':
environmentMessage =
this.context.isServedFromLocalhost
? strings.AppLocalEnvironmentTeams
: strings.AppTeamsTabEnvironment;
break;
default:
environmentMessage =
strings.UnknownEnvironment;
}
return environmentMessage;
});
}
return Promise.resolve(
this.context.isServedFromLocalhost
? strings.AppLocalEnvironmentSharePoint
: strings.AppSharePointEnvironment
);
}
protected onThemeChanged(
currentTheme: IReadonlyTheme | undefined
): void {
if (!currentTheme) {
return;
}
this._isDarkTheme = !!currentTheme.isInverted;
const { semanticColors } = currentTheme;
if (semanticColors) {
this.domElement.style.setProperty(
'--bodyText',
semanticColors.bodyText || null
);
this.domElement.style.setProperty(
'--link',
semanticColors.link || null
);
this.domElement.style.setProperty(
'--linkHovered',
semanticColors.linkHovered || null
);
}
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration():
IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description:
strings.PropertyPaneDescription
},
groups: [
{
groupName:
strings.BasicGroupName,
groupFields: [
PropertyPaneTextField(
'description',
{
label:
strings.DescriptionFieldLabel
}
),
PropertyFieldPeoplePicker(
'people',
{
label:
'Select users or groups',
initialData:
this.properties.people || [],
allowDuplicate:
false,
multiSelect:
true,
principalType: [
PrincipalType.Users,
PrincipalType.SharePoint,
PrincipalType.Security
],
onPropertyChange:
this.onPropertyPaneFieldChanged,
context:
this.context as any,
properties:
this.properties,
deferredValidationTime:
0,
key:
'peopleFieldId',
searchTextLimit:
3
}
)
]
}
]
}
]
};
}
}

How the Property Value Is Stored

Suppose two users are selected.

The resulting web part property may contain data conceptually similar to:

[
{
"id": "10",
"fullName": "Adele Vance",
"login": "i:0#.f|membership|adelev@contoso.com",
"email": "adelev@contoso.com",
"jobTitle": "Product Manager",
"initials": "AV"
},
{
"id": "21",
"fullName": "Alex Wilber",
"login": "i:0#.f|membership|alexw@contoso.com",
"email": "alexw@contoso.com",
"jobTitle": "Marketing Assistant",
"initials": "AW"
}
]

The exact stored structure can vary depending on the principal type and information returned by SharePoint.

A SharePoint group may not have an email address or job title. For this reason, optional fields should always be checked before rendering them.

Correct:

{person.email && (
<span>{person.email}</span>
)}

Risky:

<span>{person.email.toLowerCase()}</span>

The second version can fail when email is undefined.


Why IPropertyFieldGroupOrPerson Is Better Than any[]

The property could technically be declared as:

people: any[];

However, this removes TypeScript validation and editor assistance.

Using:

people: IPropertyFieldGroupOrPerson[];

provides:

  • Property name validation
  • IntelliSense
  • Better refactoring support
  • Clearer component contracts
  • Compile-time detection of invalid fields
  • Documentation directly in the code

For example, TypeScript recognizes:

person.fullName
person.login
person.email
person.jobTitle
person.imageUrl

It can also detect invalid properties:

person.userDisplayTitle

when that property is not part of the interface.


Common Problems

Selected people disappear after reloading the page

Cause:

this.properties.people = [];

is executed every time onInit() runs.

Correction:

if (!this.properties.people) {
this.properties.people = [];
}

Cannot read properties of undefined (reading 'map')

Cause:

props.people.map(...)

is called while people is undefined.

Correction:

const people = props.people || [];

or:

{(props.people || []).map(...)}

The picker does not search until three characters are typed

This is expected because of:

searchTextLimit: 3

Reduce the value when earlier searching is required:

searchTextLimit: 2

SharePoint groups are not displayed

Verify that the principal type includes:

PrincipalType.SharePoint

Example:

principalType: [
PrincipalType.Users,
PrincipalType.SharePoint
]

Security groups are not displayed

Verify that the principal type includes:

PrincipalType.Security

Example:

principalType: [
PrincipalType.Users,
PrincipalType.Security
]

The control reports a context TypeScript error

The documented configuration uses:

context: this.context

If TypeScript reports that WebPartContext is not assignable to BaseComponentContext, inspect the installed SPFx package versions before using a permanent cast.

Temporary workaround:

context: this.context as any

Preferred investigation:

npm list @microsoft/sp-component-base
npm list @microsoft/sp-module-interfaces
npm list @pnp/spfx-property-controls

The cast suppresses the compiler error but does not correct a duplicated dependency tree.


Duplicate users appear in the selection

Set:

allowDuplicate: false

This prevents the same principal from being selected repeatedly.


Practical Use Cases

The PropertyFieldPeoplePicker can be used to configure:

  • Document owners
  • Page approvers
  • Reviewers
  • Notification recipients
  • Project managers
  • Department contacts
  • Support representatives
  • Escalation contacts
  • Members allowed to view custom content
  • SharePoint groups used for security trimming
  • Users passed to Power Automate
  • Users used in Microsoft Graph or SharePoint REST requests

The control itself selects and stores principals. Additional business logic is still required to send notifications, assign permissions, start approval flows, or perform other actions.


Property Pane Control Versus Page People Picker

It is important to distinguish between two different PnP controls.

PropertyFieldPeoplePicker

Package:

@pnp/spfx-property-controls

Purpose:

Used by the web part author inside the property pane.

Typical example:

PropertyFieldPeoplePicker('people', {
...
})

PeoplePicker

Package:

@pnp/spfx-controls-react

Purpose:

Rendered directly inside the React user interface of the web part.

The first configures the web part. The second is part of the visible page experience.

For this implementation, PropertyFieldPeoplePicker is the correct control because the selection is made through the web part property pane.


Final Result

The completed web part allows an author to:

  1. Open the web part property pane.
  2. Search for users, SharePoint groups, or security groups.
  3. Select multiple principals.
  4. Prevent duplicate selections.
  5. Store the selected principals in the web part properties.
  6. Pass the stored objects to a React functional component.
  7. Render the users and groups on the page.
  8. Preserve the selection after the page is saved and reloaded.

The most important implementation detail is to avoid clearing this.properties.people unconditionally during onInit().

Correct initialization:

if (!this.properties.people) {
this.properties.people = [];
}

With this correction, the PropertyFieldPeoplePicker becomes a reliable way to configure user and group selections in an SPFx web part.

Official Reference

PropertyFieldPeoplePicker – PnP SPFx Property Controls

Edvaldo Guimrães Filho Avatar

Published by