Last week, one of my clients asked for a small but interesting customization. They wanted to change the default SharePoint list form and make it look more modern and user-friendly.
They specifically wanted to replace the default choice field with a Fluent UI Dropdown. The dropdown needed to read values from a SharePoint choice field, show icons for each option, disable a few options, and display the default value already set on the item. In some cases, they also needed multi-select support.
All of this can be done using Fluent UI React controls inside a SharePoint Framework (SPFx) Form Customizer. No custom HTML. Just clean and supported components.
In this tutorial, I’ll show you how to use the Fluent UI Dropdown in SPFx and bind it to a SharePoint choice field. By the end of this post, you’ll know how to populate dropdown options, set default values, disable items, and enhance the form with icons.
If you want to learn SharePoint Framework development from scratch, check out the SPFx training course.
Fluent UI React Dropdown in SPFx Form Customizer
To customize the SharePoint list form, I created an SPFx Form Customizer extension using React. We will use the following SharePoint list, which has the following columns, and the SharePoint list looks like the one shown in the image below.

The above list has these columns:
| Column Name | Data Type |
|---|---|
| ProjectName [Title ] | Default field |
| ProjectDescription | Multiple lines of text |
| ProjectCategory | Choice(IT Project, Marketing Campaign, Product Launch, Infrastructure Upgrade) |
| TechnologiesRequired | Multi select Choice(SharePoint, Power BI, Dynamics 365, SQL Server, Power Apps, Power Automate) |
| BusinessUnit | Choice(Sales, Finance, HR, IT, Operations) |
| Priority | Choice(Low, Medium, High) |
In the example below, you can see the customized form. Within it, I used the Fluent UI React Dropdown control for the choice fields, the Text control for single- and multi-line text fields, and button controls.
- The default value of the “Project Category” field is auto-populated in the dropdown.
- For the “Technology Required” field, added a multi-select dropdown control with icons and a disabled option.
- The “Business Unit” field uses the standard dropdown control.
- The “Priority” field has the icons for the options.
Clicking the save button saves the form input to the SharePoint list using PnPJS methods.

Now, let’s see the steps to achieve this example.
- Open the command prompt and run the commands below to create a new directory.
md FluentUIDropdown
cd FluentUIDropdown
- Once it is created, run the command below to quickly create a SharePoint client-side solution project with the right toolchain and project structure.
yo @microsoft/sharepoint
- What is your solution name? fluent-ui-dropdown
- Which type of client-side component to create? Extension
- Which type of client-side extension to create? Form Customizer
- Add new Web part to solution fluent-ui-dropdown.
- What is your Web part name? FrmProjectRequests
- Which template would you like to use? React

- To install the PnPJS library into our solution, run the command below.
npm install @pnp/sp --save
- To open the spfx form customizer, run the command below to open Visual Studio Code.
Code .
- Open the “FrmProjectRequestsFormCustomizer.ts” file and then replace the default code with the code below.
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Log } from '@microsoft/sp-core-library';
import {
BaseFormCustomizer
} from '@microsoft/sp-listview-extensibility';
import FrmProjectRequests, { IFrmProjectRequestsProps } from './components/FrmProjectRequests';
import { spfi, SPFx } from "@pnp/sp";
import { SPFI } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import "@pnp/sp/fields";
export interface IFrmProjectRequestsFormCustomizerProperties {
sampleText?: string;
}
const LOG_SOURCE: string = 'FrmProjectRequestsFormCustomizer';
export default class FrmProjectRequestsFormCustomizer
extends BaseFormCustomizer<IFrmProjectRequestsFormCustomizerProperties> {
private _sp: SPFI
public onInit(): Promise<void> {
Log.info(LOG_SOURCE, 'Activated FrmProjectRequestsFormCustomizer with properties:');
Log.info(LOG_SOURCE, JSON.stringify(this.properties, undefined, 2));
this._sp = spfi().using(SPFx(this.context));
return Promise.resolve();
}
public render(): void {
const frmProjectRequests: React.ReactElement<IFrmProjectRequestsProps> =
React.createElement(FrmProjectRequests, {
context: this.context,
sp:this._sp,
displayMode: this.displayMode,
onSave: this._onSave,
onClose: this._onClose
} as IFrmProjectRequestsProps);
ReactDOM.render(frmProjectRequests, this.domElement);
}
public onDispose(): void {
// This method should be used to free any resources that were allocated during rendering.
ReactDOM.unmountComponentAtNode(this.domElement);
super.onDispose();
}
private _onSave = (): void => {
this.formSaved();
}
private _onClose = (): void => {
this.formClosed();
}
}
Here:
- We imported the PnPJS statements required to interact with lists, items, and fields.
- import “@pnp/sp/webs”;
- import “@pnp/sp/lists”;
- import “@pnp/sp/items”;
- import “@pnp/sp/fields”;
- Then, I added a variable named _sp and assigned it the SPFI data type. This variable holds the PnPJS instance.
- In the onInit() method, we created the PnPJS instance and assigned it to the variable we created above [this._sp = spfi().using(SPFx(this.context))].
- Then, in the render() method, we passed the sp as a prop to the React component.
- Now open the “FrmProjectRequests.tsx” file and update your default code with the code below.
import * as React from "react";
import { Log, FormDisplayMode } from "@microsoft/sp-core-library";
import { FormCustomizerContext } from "@microsoft/sp-listview-extensibility";
import { SPFI } from "@pnp/sp";
import { TextField } from "@fluentui/react/lib/TextField";
import { Dropdown, IDropdownOption, DropdownMenuItemType } from "@fluentui/react/lib/Dropdown";
import { PrimaryButton, DefaultButton } from "@fluentui/react/lib/Button";
import { Stack } from "@fluentui/react/lib/Stack";
import { Icon } from "@fluentui/react/lib/Icon";
import {
CheckmarkCircle24Filled,
Warning24Filled,
ErrorCircle24Filled
} from '@fluentui/react-icons';
import styles from "./FrmProjectRequests.module.scss";
export interface IFrmProjectRequestsProps {
context: FormCustomizerContext;
displayMode: FormDisplayMode;
sp: SPFI;
onSave: () => void;
onClose: () => void;
}
export interface IFrmProjectRequestsState {
loading: boolean;
projectName: string;
projectDescription: string;
projectCategoryOptions: IDropdownOption[];
technologiesOptions: IDropdownOption[];
businessUnitOptions: IDropdownOption[];
priorityOptions: IDropdownOption[];
projectCategory?: string;
technologiesRequired: string[];
businessUnit?: string;
priority?: string;
}
const LOG_SOURCE = "FrmProjectRequests";
export default class FrmProjectRequests extends React.Component<
IFrmProjectRequestsProps,
IFrmProjectRequestsState
> {
constructor(props: IFrmProjectRequestsProps) {
super(props);
this.state = {
loading: true,
projectName: "",
projectDescription: "",
projectCategoryOptions: [],
technologiesOptions: [],
businessUnitOptions: [],
priorityOptions: [],
projectCategory: undefined,
technologiesRequired: [],
businessUnit: undefined,
priority: undefined,
};
}
private iconStyles = { marginRight: 8 };
private onRenderOption = (option: IDropdownOption): JSX.Element => {
return (
<div>
{option.data && option.data.icon && (
<Icon style={this.iconStyles} iconName={option.data.icon} />
)}
<span>{option.text}</span>
</div>
);
};
private onRenderTitle = (options: IDropdownOption[]): JSX.Element => {
const opt = options[0];
return (
<div>
{opt.data && opt.data.icon && (
<Icon style={this.iconStyles} iconName={opt.data.icon} />
)}
<span>{opt.text}</span>
</div>
);
};
private buildTechnologyOptions = (choices: string[]): IDropdownOption[] => {
const groups: {
cloud: string[];
businessApps: string[];
dataPlatforms: string[];
} = {
cloud: [],
businessApps: [],
dataPlatforms: []
};
for (let i = 0; i < choices.length; i++) {
const c = choices[i];
if (
c.indexOf("SharePoint") >= 0 ||
c.indexOf("Power Automate") >= 0 ||
c.indexOf("Power Apps") >= 0
) {
groups.cloud.push(c);
} else if (c.indexOf("Dynamics") >= 0) {
groups.businessApps.push(c);
} else if (c.indexOf("SQL") >= 0) {
groups.dataPlatforms.push(c);
}
}
const getIcon = (c: string): string | undefined => {
if (c.indexOf("SharePoint") >= 0) return "SharepointLogo";
if (c.indexOf("Power Apps") >= 0) return "PowerApps";
if (c.indexOf("Power Automate") >= 0) return "Flow";
if (c.indexOf("Dynamics") >= 0) return "Dynamics365Logo";
if (c.indexOf("SQL") >= 0) return "Database";
return undefined;
};
const results: IDropdownOption[] = [];
if (groups.cloud.length > 0) {
results.push({
key: "hdrCloud",
text: "Microsoft Cloud",
itemType: DropdownMenuItemType.Header
});
for (let i = 0; i < groups.cloud.length; i++) {
const c = groups.cloud[i];
results.push({
key: c,
text: c,
data: { icon: getIcon(c) }
});
}
results.push({ key: "div1", text: "-", itemType: DropdownMenuItemType.Divider });
}
if (groups.businessApps.length > 0) {
results.push({
key: "hdrBizApps",
text: "Business Applications",
itemType: DropdownMenuItemType.Header
});
for (let i = 0; i < groups.businessApps.length; i++) {
const c = groups.businessApps[i];
results.push({
key: c,
text: c,
data: { icon: getIcon(c) }
});
}
results.push({ key: "div2", text: "-", itemType: DropdownMenuItemType.Divider });
}
if (groups.dataPlatforms.length > 0) {
results.push({
key: "hdrDB",
text: "Database Platforms",
itemType: DropdownMenuItemType.Header
});
for (let i = 0; i < groups.dataPlatforms.length; i++) {
const c = groups.dataPlatforms[i];
results.push({
key: c,
text: c,
data: { icon: getIcon(c) }
});
}
}
return results;
};
public async componentDidMount(): Promise<void> {
Log.info(LOG_SOURCE, "FrmProjectRequests mounted");
await this.loadDropdownChoices();
}
public componentWillUnmount(): void {
Log.info(LOG_SOURCE, "FrmProjectRequests unmounted");
}
private loadDropdownChoices = async (): Promise<void> => {
try {
const list = this.props.sp.web.lists.getByTitle("ProjectRequests");
const projectCategoryField = await list.fields.getByInternalNameOrTitle("ProjectCategory")();
const technologiesField = await list.fields.getByInternalNameOrTitle("TechnologiesRequired")();
const businessUnitField = await list.fields.getByInternalNameOrTitle("BusinessUnit")();
const priorityField = await list.fields.getByInternalNameOrTitle("Priority")();
const defaultCategory = projectCategoryField.DefaultValue || undefined;
this.setState({
projectCategoryOptions: (projectCategoryField.Choices || []).map((c: string) => ({ key: c, text: c })),
technologiesOptions: this.buildTechnologyOptions(technologiesField.Choices || []),
businessUnitOptions: (businessUnitField.Choices || []).map(c => ({ key: c, text: c })),
priorityOptions: this.buildPriorityOptions(priorityField.Choices || []),
projectCategory: this.props.displayMode === FormDisplayMode.New ? defaultCategory : this.state.projectCategory,
loading: false
});
} catch (error) {
console.error("Error loading dropdown choices:", error);
this.setState({ loading: false });
}
};
private onSaveForm = async (): Promise<void> => {
try {
await this.props.sp.web.lists.getByTitle("ProjectRequests").items.add({
Title: this.state.projectName,
ProjectDescription: this.state.projectDescription,
ProjectCategory: this.state.projectCategory,
TechnologiesRequired: this.state.technologiesRequired,
BusinessUnit: this.state.businessUnit,
Priority: this.state.priority,
});
this.props.onSave();
} catch (err) {
console.error("Error saving new item:", err);
}
};
private renderPriorityOption = (option: IDropdownOption): JSX.Element => {
const IconComp = option.data?.icon;
return (
<div style={{ display: 'flex', alignItems: 'center' }}>
{IconComp && <IconComp style={{ marginRight: 8 }} />}
<span>{option.text}</span>
</div>
);
};
private renderPriorityTitle = (options: IDropdownOption[]): JSX.Element => {
const option = options[0];
const IconComp = option.data?.icon;
return (
<div style={{ display: 'flex', alignItems: 'center' }}>
{IconComp && <IconComp style={{ marginRight: 8 }} />}
<span>{option.text}</span>
</div>
);
};
private buildPriorityOptions = (choices: string[]): IDropdownOption[] => {
const results: IDropdownOption[] = [];
const getIcon = (value: string): any => {
if (value.indexOf("Low") >= 0) return CheckmarkCircle24Filled;
if (value.indexOf("Medium") >= 0) return Warning24Filled;
if (value.indexOf("High") >= 0) return ErrorCircle24Filled;
return undefined;
};
for (let i = 0; i < choices.length; i++) {
const c = choices[i];
results.push({
key: c,
text: c,
data: { icon: getIcon(c) }
});
}
return results;
};
public render(): React.ReactElement<IFrmProjectRequestsProps> {
if (this.state.loading) return <div>Loading...</div>;
return (
<div className={styles.pageWrapper}>
<div className={styles.formCard}>
<Stack tokens={{ childrenGap: 20 }}>
<TextField
label="Project Name"
value={this.state.projectName}
required
className={styles.fullWidthField}
onChange={(e, v) => this.setState({ projectName: v || "" })}
/>
<TextField
label="Project Description"
multiline
rows={4}
className={styles.fullWidthField}
value={this.state.projectDescription}
onChange={(e, v) => this.setState({ projectDescription: v || "" })}
/>
<div className={styles.row2Cols}>
<Dropdown
label="Project Category"
placeholder="Select"
options={this.state.projectCategoryOptions}
selectedKey={this.state.projectCategory}
onChange={(e, option) => this.setState({ projectCategory: option?.key as string })}
/>
<Dropdown
label="Technologies Required"
placeholder="Select multiple"
multiSelect
options={this.state.technologiesOptions}
onRenderOption={this.onRenderOption}
onRenderTitle={this.onRenderTitle}
selectedKeys={this.state.technologiesRequired}
onChange={(e, option) => {
const key = option!.key as string;
const updated = option!.selected
? [...this.state.technologiesRequired, key]
: this.state.technologiesRequired.filter(k => k !== key);
this.setState({ technologiesRequired: updated });
}}
/>
</div>
<div className={styles.row2Cols}>
<Dropdown
label="Business Unit"
placeholder="Select"
options={this.state.businessUnitOptions}
selectedKey={this.state.businessUnit}
onChange={(e, option) => this.setState({ businessUnit: option?.key as string })}
/>
<Dropdown
label="Priority"
placeholder="Select"
options={this.state.priorityOptions}
selectedKey={this.state.priority}
onRenderOption={this.renderPriorityOption}
onRenderTitle={this.renderPriorityTitle}
onChange={(e, option) => this.setState({ priority: option?.key as string })}
/>
</div>
<Stack horizontal tokens={{ childrenGap: 10 }}>
<PrimaryButton text="Save" onClick={this.onSaveForm} />
<DefaultButton text="Cancel" onClick={this.props.onClose} />
</Stack>
</Stack>
</div>
</div>
);
}
}
Here:
- At the start, we imported the following Fluent UI React controls and icons:
- Dropdown, IDropdownOption, DropdownMenuItemType
- TextField,
- PrimaryButton,
- DefaultButton,
- Stack,
- Icon
- CheckmarkCircle24Filled
- Warning24Filled
- ErrorCircle24Filled
- IFrmProjectRequestsProps{}= Within this, we retrieved the sp prop.
- IFrmProjectRequestsState = I created this state to store both the dropdown options fetched from SharePoint and the values entered by the user.
- loading =It shows a loading message until the dropdown data is fetched.
- projectName & projectDescription = store the text entered in the form fields.
- projectCategoryOptions = holds the list of categories loaded from SharePoint.
- technologiesOptions = stores the grouped technology choices with icons.
- businessUnitOptions = contains all business unit options from the list.
- priorityOptions = holds priority values along with custom icons.
- projectCategory = saves the user’s selected project category.
- technologiesRequired = stores multiple selected technologies.
- businessUnit = keeps the chosen business unit.
- priority = stores the user-selected priority.
- Within the constructor(), we initialized those states with empty values.
- In the componentDidMount() method, we called the loadDropdownChoices() method.
- In the loadDropdownChoices() method, we use the PnP JS method to fetch data from the “ProjectRequests” SharePoint list.
- ProjectCategory column values are assigned to the “projectCategoryField” state.
- TechnologiesRequired column values assigned to the “technologiesField” state.
- BusinessUnit values assigned to the “businessUnitField” state.
- Priority values are assigned to the “priorityOptions” state.
- defaultCategory stores the default value of the project category field.
- buildTechnologyOptions() = We called this method while fetching the “TechnologiesRequired” field values by passing the choices that we fetch from the list:
- To display choice values under a heading, we created three groups, which are string arrays:
- cloud
- businessApps
- dataPlatforms
- For(){} = This loop iterates over each technology in the choices array and checks its category. Based on the text it contains, the technology is pushed into the group:
- If the name includes SharePoint, Power Apps, or Power Automate, it is added to the cloud group.
- If it contains Dynamics, it is added to the businessApps group.
- If it contains SQL, it is added to the dataPlatforms group.
- getIcon = Adding icons to each technology.
- results = Created an empty array of type IDropdownOption[] and added a header under the cloud group values added, then another header. After that, the businessApps group is pushed into, and the same for the remaining group as well.
- To display choice values under a heading, we created three groups, which are string arrays:
- buildPriorityOptions() = Like in the above, this method also performs the same action for the Priority values.
- onRenderOption and onRenderTitle are used to display the technology choices in a dropdown control with icons and options side by side.
- renderPriorityTitle and renderPriorityOption are also used for the same purpose as above, but for the Priority field.
- onSaveForm() = This method fetches the form input and saves it to the SharePoint list using PnPJS.
- render() = In this render() method, we display the Fluent UI React controls in a two-column layout.
- For the Dropdown control’s onRenderOption and onRenderTitle properties, we are calling the methods we created above.
- To apply styles, add the below CSS styles to your.SCSS file.
.frmProjectRequests {
background-color: "[theme:white, default:#ffffff]";
color: "[theme:themePrimary, default:#0078d4]";
padding: 0.5rem;
}
.pageWrapper {
background-color: #f3f3f3;
padding: 30px;
display: flex;
justify-content: center;
}
.formCard {
background: #ffffff;
padding: 30px 40px;
width: 900px;
border-radius: 10px;
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
}
.fullWidthField {
width: 100%;
}
.row2Cols {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 25px;
}
- Now save the changes, and run the command below to test the form before deployment.
gulp serve --config frmProjectRequests_NewForm
Here, frmProjectRequests_NewForm is the name of our new form; you can get it from the serve.json file.
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json",
"port": 4321,
"https": true,
"serveConfigurations": {
"default": {
"pageUrl": "https://szg52.sharepoint.com/sites/SPFXDevelopment/_layouts/15/SPListForm.aspx",
"formCustomizer": {
"componentId": "a35bc6fc-8bb7-4317-9031-acfbb19d58cc",
"PageType": 8,
"RootFolder": "/sites/SPFXDevelopment/Lists/ProjectRequests",
"properties": {
"sampleText": "Value"
}
}
},
"frmProjectRequests_NewForm": {
"pageUrl": "https://szg52.sharepoint.com/sites/SPFXDevelopment/_layouts/15/SPListForm.aspx",
"formCustomizer": {
"componentId": "a35bc6fc-8bb7-4317-9031-acfbb19d58cc",
"PageType": 8,
"RootFolder": "/sites/SPFXDevelopment/Lists/ProjectRequests",
"properties": {
"sampleText": "Value"
}
}
},
"frmProjectRequests_EditForm": {
"pageUrl": "https://szg52.sharepoint.com/sites/SPFXDevelopment/_layouts/15/SPListForm.aspx",
"formCustomizer": {
"componentId": "a35bc6fc-8bb7-4317-9031-acfbb19d58cc",
"PageType": 6,
"RootFolder": "/sites/SPFXDevelopment/Lists/ProjectRequests",
"ID": 1,
"properties": {
"sampleText": "Value"
}
}
},
"frmProjectRequests_ViewForm": {
"pageUrl": "https://szg52.sharepoint.com/sites/SPFXDevelopment/_layouts/15/SPListForm.aspx",
"formCustomizer": {
"componentId": "a35bc6fc-8bb7-4317-9031-acfbb19d58cc",
"PageType": 4,
"RootFolder": "/sites/SPFXDevelopment/Lists/ProjectRequests",
"ID": 1,
"properties": {
"sampleText": "Value"
}
}
}
}
}
Here, the “serveConfigurations” contains four objects, each representing:
- “default”:{} = It loads your form customizer in its default mode for quick testing.
- “frmProjectRequests_NewForm”: {} = Loads the form customizer in New Form mode [PageType: 8].
- “frmProjectRequests_EditForm”: {} = Loads the extension in Edit Form mode [PageType: 6] for a specific item ID.
- “frmProjectRequests_ViewForm”: {} = Opens the View Form mode [PageType: 4] for a specific item ID.
- For the “pageUrl” parameter in the above configuration, provide your SharePoint site URL.
- For the “RootFolder” parameter, provide your site name, list name in URL format as I gave above.
That’s it, now you can deploy this solution to the App Catalog.
I hope you found this article helpful. In this article, you learned how to use the Fluent UI Dropdown in an SPFx Form Customizer. We covered how to bind SharePoint choice field values, display the default value, disable specific options, add custom icons, and save the selected values back to the list.
Download the solution, update the list and column names, and try it with your own SharePoint list. If you run into any issues or have questions while testing, feel free to leave a comment below.
Also, check out the blog posts below on SPFx Fluent UI React controls:
- SPFx Fluent UI React Control: ChoiceGroup and Checkbox
- SharePoint Framework (SPFx) Fluent UI Basic List Example
- Fluent UI React ComboBox Control in SPFx
- Fluent UI DocumentCard in SPFx Web Part

Hey! I’m Bijay Kumar, founder of SPGuides.com and a Microsoft Business Applications MVP (Power Automate, Power Apps). I launched this site in 2020 because I truly enjoy working with SharePoint, Power Platform, and SharePoint Framework (SPFx), and wanted to share that passion through step-by-step tutorials, guides, and training videos. My mission is to help you learn these technologies so you can utilize SharePoint, enhance productivity, and potentially build business solutions along the way.
How do you handle keeping the DropDown value when something in the PropertyPane is changed triggers a ‘componentDidUpdate’? When this happens, the dropdown selection is cleared even though the state still exists for the selected key.
Figured it out. I wasn’t properly awaiting the calls to fetch the dropdown options. As a result, the options didn’t exist yet to properly set the existing selected key.
Can we fetch the dropdown values on component itself? , we are pulling data on .ts file and not on .tsx
This is worked well, Thank you so much!!