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
targetSiteUrlis 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.UsersPrincipalType.SharePointPrincipalType.Security
Their meanings are:
| Principal type | Purpose |
|---|---|
PrincipalType.Users | Searches for users |
PrincipalType.SharePoint | Searches for SharePoint groups |
PrincipalType.Security | Searches 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-basenpm list @microsoft/sp-module-interfacesnpm list @pnp/spfx-property-controls
If multiple incompatible SPFx package versions appear, clean and reinstall the dependencies:
Remove-Item -Path ".\node_modules" -Recurse -ForceRemove-Item -Path ".\package-lock.json" -Forcenpm 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.fullNameperson.loginperson.emailperson.jobTitleperson.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-basenpm list @microsoft/sp-module-interfacesnpm 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:
- Open the web part property pane.
- Search for users, SharePoint groups, or security groups.
- Select multiple principals.
- Prevent duplicate selections.
- Store the selected principals in the web part properties.
- Pass the stored objects to a React functional component.
- Render the users and groups on the page.
- 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.
