If you’ve built custom forms in SharePoint Framework, you already know the pain — a user submits a form with a blank required field, or types random text into an email field, and suddenly your web part breaks or saves garbage data to a SharePoint list.
Good form validation catches all of that before it becomes your problem.
In this tutorial, I’ll walk you through every practical way to do form validation in SPFx — from the simple built-in property pane approach to React Hook Form with Fluent UI components, Formik with Yup, and the PnP DynamicForm control. I’ll give you real code for each one so you can pick what fits your situation.
If you want to learn SharePoint Framework development, check out our SharePoint Framework training course.
SharePoint Framework (SPFx) Form Validation
SPFx forms fall into two buckets:
- Property pane fields — the side panel where users configure your web part
- Custom React forms — forms you build inside the web part itself (think “Submit a Request” forms, data entry panels, etc.)
Both need validation, but they work differently. Let’s cover both.
Method 1: Property Pane Validation with onGetErrorMessage
This is the most common starting point. If you have a property pane text field and want to make sure the user provides a valid value, SPFx gives you a built-in hook called onGetErrorMessage.
Inline Validation (No API Needed)
Say you have a “Description” field in your property pane and you want it to:
- Not be empty
- Stay under 40 characters
Here’s how you wire that up:
private validateDescription(value: string): string {
if (value === null || value.trim().length === 0) {
return 'Please provide a description.';
}
if (value.length > 40) {
return 'Description should not be longer than 40 characters.';
}
return ''; // Empty string = no error
}

Then in your getPropertyPaneConfiguration(), attach it:
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel,
onGetErrorMessage: this.validateDescription.bind(this)
})
The rule is simple: return an error string if invalid, return an empty string if valid. SharePoint Framework handles displaying the red error message automatically.
One nice side effect: when a property pane field has a validation error, the Apply button is automatically disabled. Users literally can’t save a broken configuration.
Remote API Validation (Check if a List Exists)
Sometimes, inline logic isn’t enough. Say you have a field where users type a SharePoint list name. You want to verify that the list actually exists on the site — that’s a job for the SharePoint REST API.
Here’s how you do it with an async method:
private async validateListName(value: string): Promise<string> {
if (value === null || value.length === 0) {
return 'Please provide the list name.';
}
try {
const response = await this.context.spHttpClient.get(
this.context.pageContext.web.absoluteUrl +
`/_api/web/lists/getByTitle('${escape(value)}')?$select=Id`,
SPHttpClient.configurations.v1
);
if (response.ok) {
return '';
} else if (response.status === 404) {
return `The list '${escape(value)}' doesn't exist on this site.`;
} else {
return `Something went wrong: ${response.statusText}. Please try again.`;
}
} catch (error) {
return error.message;
}
}

And attach it the same way:
PropertyPaneTextField('listName', {
label: strings.ListNameFieldLabel,
onGetErrorMessage: this.validateListName.bind(this),
deferredValidationTime: 500
})
Notice deferredValidationTime: 500. This tells SPFx to wait 500 milliseconds after the user stops typing before firing the validation. Without this, it would hit the SharePoint API on every single keystroke. That’s noisy, slow, and unnecessary. The default is 200ms — bump it up for API calls.

Check out SPFx Environment Variables
Method 2: Manual Validation with React useState
If you’re building a custom React form inside a web part (not the property pane), the most lightweight approach is managing validation state yourself with hooks.
This is perfect for simple forms where you don’t want to pull in extra libraries.
import * as React from 'react';
import { useState } from 'react';
import { TextField, PrimaryButton } from '@fluentui/react';
const SimpleForm: React.FC = () => {
const [title, setTitle] = useState('');
const [email, setEmail] = useState('');
const [errors, setErrors] = useState<{ title?: string; email?: string }>({});
const validate = (): boolean => {
const newErrors: { title?: string; email?: string } = {};
if (!title.trim()) {
newErrors.title = 'Title is required.';
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email.trim()) {
newErrors.email = 'Email is required.';
} else if (!emailRegex.test(email)) {
newErrors.email = 'Please enter a valid email address.';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (): void => {
if (validate()) {
console.log('Form submitted:', { title, email });
// Save to SharePoint list here
}
};
return (
<div>
<TextField
label="Title"
value={title}
onChange={(_, val) => setTitle(val || '')}
errorMessage={errors.title}
required
/>
<TextField
label="Email"
value={email}
onChange={(_, val) => setEmail(val || '')}
errorMessage={errors.email}
required
/>
<PrimaryButton text="Submit" onClick={handleSubmit} style={{ marginTop: 16 }} />
</div>
);
};
export default SimpleForm;
What I like about this approach:
- Zero additional packages
- Full control over when validation fires (on submit, on blur, wherever you want)
- Works great with Fluent UI’s built-in
errorMessageprop onTextField
The errorMessage prop on Fluent UI’s TextField component renders the red error text right below the field — you don’t need to add any CSS.

Check out Set Up Your SharePoint Framework (SPFx) Development Environment using Heft Toolchain
Method 3: React Hook Form + Fluent UI
For anything more complex than 2-3 fields, I’d recommend React Hook Form (RHF). It handles form state, validation, and submission in a clean, hook-based way. The catch is, it doesn’t know about Fluent UI components by default — you have to connect them using the Controller wrapper.
Install it first:
npm install react-hook-form
Basic example:
import * as React from 'react';
import { useForm, Controller } from 'react-hook-form';
import { TextField, Dropdown, PrimaryButton } from '@fluentui/react';
interface IFormData {
projectName: string;
category: string;
}
const RHFForm: React.FC = () => {
const {
handleSubmit,
control,
formState: { errors }
} = useForm<IFormData>();
const onSubmit = (data: IFormData): void => {
console.log('Valid data:', data);
// Save to SharePoint here
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="projectName"
control={control}
rules={{
required: 'Project name is required.',
minLength: { value: 3, message: 'Minimum 3 characters.' },
maxLength: { value: 50, message: 'Maximum 50 characters.' }
}}
render={({ field }) => (
<TextField
label="Project Name"
{...field}
errorMessage={errors.projectName?.message}
required
/>
)}
/>
<Controller
name="category"
control={control}
rules={{ required: 'Please select a category.' }}
render={({ field }) => (
<Dropdown
label="Category"
options={[
{ key: 'dev', text: 'Development' },
{ key: 'design', text: 'Design' },
{ key: 'qa', text: 'QA' }
]}
selectedKey={field.value}
onChange={(_, option) => field.onChange(option?.key)}
errorMessage={errors.category?.message}
/>
)}
/>
<PrimaryButton text="Submit" type="submit" style={{ marginTop: 16 }} />
</form>
);
};
export default RHFForm;
The rules object inside Controller supports:
required— field cannot be emptyminLength/maxLength— character limitspattern— regex matchingvalidate— custom function returningtrueor an error string
Custom validation with validate:
rules={{
validate: (value) => {
if (value.includes('test')) {
return 'Project name cannot contain the word "test".';
}
return true;
}
}}

Method 4: Formik + Yup
Formik is another popular library for form management, and it pairs really well with Yup for schema-based validation. If you’re familiar with object schemas and prefer declaring validation rules as a single schema object rather than per-field rules, this combo is clean and readable.
Install both:
npm install formik yup
Example form with a SharePoint list submission:
Add the following code to the FormikForm.tsx file.
import * as React from 'react';
import { useState } from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
import { PrimaryButton, MessageBar, MessageBarType } from '@fluentui/react';
interface ITaskFormValues {
taskTitle: string;
startDate: string;
endDate: string;
email: string;
}
const validationSchema = Yup.object({
taskTitle: Yup.string()
.required('Task title is required.')
.max(100, 'Max 100 characters.'),
startDate: Yup.date()
.required('Start date is required.')
.nullable(),
endDate: Yup.date()
.required('End date is required.')
.nullable()
.min(Yup.ref('startDate'), 'End date must be after start date.'),
email: Yup.string()
.email('Must be a valid email address.')
.required('Email is required.')
});
const initialValues: ITaskFormValues = {
taskTitle: '',
startDate: '',
endDate: '',
email: ''
};
const fieldStyle: React.CSSProperties = {
display: 'block',
width: '100%',
padding: '4px 8px',
marginTop: 4,
border: '1px solid #ccc',
borderRadius: 2,
fontSize: 14
};
const errorStyle: React.CSSProperties = {
color: '#a4262c',
fontSize: 12,
marginTop: 4
};
const FormikForm: React.FC = () => {
const [submittedData, setSubmittedData] = useState<ITaskFormValues | null>(null);
const handleReset = (): void => {
setSubmittedData(null);
};
if (submittedData) {
return (
<div>
<MessageBar messageBarType={MessageBarType.success}>
Task saved! <strong>{submittedData.taskTitle}</strong> — {submittedData.startDate} to {submittedData.endDate} ({submittedData.email})
</MessageBar>
<PrimaryButton text="Submit Another" onClick={handleReset} style={{ marginTop: 16 }} />
</div>
);
}
return (
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={(values: ITaskFormValues) => {
setSubmittedData(values);
// Save to SharePoint list here
}}
>
{() => (
<Form>
<div style={{ marginBottom: 12 }}>
<label>Task Title</label>
<Field name="taskTitle" type="text" style={fieldStyle} />
<ErrorMessage name="taskTitle">{(msg) => <div style={errorStyle}>{msg}</div>}</ErrorMessage>
</div>
<div style={{ marginBottom: 12 }}>
<label>Start Date</label>
<Field name="startDate" type="date" style={fieldStyle} />
<ErrorMessage name="startDate">{(msg) => <div style={errorStyle}>{msg}</div>}</ErrorMessage>
</div>
<div style={{ marginBottom: 12 }}>
<label>End Date</label>
<Field name="endDate" type="date" style={fieldStyle} />
<ErrorMessage name="endDate">{(msg) => <div style={errorStyle}>{msg}</div>}</ErrorMessage>
</div>
<div style={{ marginBottom: 12 }}>
<label>Email</label>
<Field name="email" type="email" style={fieldStyle} />
<ErrorMessage name="email">{(msg) => <div style={errorStyle}>{msg}</div>}</ErrorMessage>
</div>
<PrimaryButton text="Save Task" type="submit" style={{ marginTop: 16 }} />
</Form>
)}
</Formik>
);
};
export default FormikForm;
The Yup.ref('startDate') part is genuinely useful — it lets you validate one field relative to another. That cross-field date comparison is something you’d have to write manually in the other approaches.
Then create one more FormikFormValidations.tsx file and enter the following code:
import * as React from 'react';
import type { IFormikFormValidationProps } from './IFormikFormValidationProps';
import FormikForm from './FormikForm';
export default class FormikFormValidation extends React.Component<IFormikFormValidationProps> {
public render(): React.ReactElement<IFormikFormValidationProps> {
return (
<section style={{ padding: '1em' }}>
<h2>Method 4: Formik + Yup</h2>
<FormikForm />
</section>
);
}
}
After running the web part, you will see the form with validations such as mandatory fields, the end date must be after the start date, etc.

When to pick Formik + Yup over React Hook Form:
- You have a lot of cross-field dependencies (date ranges, conditional fields)
- Your team already uses Yup schemas elsewhere
- You prefer a declarative schema over rules scattered through JSX
When React Hook Form wins:
- You’re using Fluent UI controls heavily (RHF’s
Controllerplays nicer) - You want less bundle size (RHF is smaller)
- You need real-time field-level validation as the user types
Method 5: PnP DynamicForm Control
If your form is basically a SharePoint list form — where users are creating or editing list items — don’t build validation from scratch. The PnP DynamicForm control reads your list schema, auto-generates the form, and handles required field validation automatically.
Install the PnP controls:
npm install @pnp/spfx-controls-react --save --save-exact
Use it in your web part:
import { DynamicForm } from '@pnp/spfx-controls-react/lib/DynamicForm';
// Inside your render method:
<DynamicForm
context={this.props.context}
listId={"your-list-guid-here"}
listItemId={1} // omit this for new items
onCancelled={() => console.log('Cancelled')}
onBeforeSubmit={async (listItem) => {
// Return true to cancel the submission
if (!listItem.Title) {
alert('Title is required.');
return true;
}
return false;
}}
onSubmitError={(listItem, error) => {
alert(`Error saving: ${error.message}`);
}}
onSubmitted={async (listItemData) => {
console.log('Saved:', listItemData);
}}
/>
You can also show a validation error dialog instead of alerts:
validationErrorDialogProps={{
showDialogOnValidationError: true,
customTitle: 'Please fix the errors below',
customMessage: 'Some required fields are missing.'
}}
And if you want to hide certain fields or make them read-only:
hiddenFields={['InternalFieldName1']}
disabledFields={['ReadOnlyField']}

This is easily the fastest option if your form maps directly to a SharePoint list. You get required field validation out of the box — just make sure your list columns are marked as required in the list settings, and DynamicForm will enforce that automatically.
A Quick Comparison
| Scenario | Best Approach |
|---|---|
| Property pane text/number fields | onGetErrorMessage (built-in) |
| Simple 2-3 field custom form | Manual useState validation |
| Complex multi-field React form | React Hook Form + Fluent UI |
| Forms with cross-field date/value rules | Formik + Yup |
| CRUD form for a SharePoint list | PnP DynamicForm control |
| Validate against SharePoint API | Async onGetErrorMessage with deferredValidationTime |
A Few Tips I’d Pass On
- Don’t validate on every keystroke for API calls. Always use
deferredValidationTime(at least 500ms) when your validation hits an endpoint. Otherwise, you’re spamming requests every time the user types a letter. - Keep error messages human. “This field is required” is fine. “Null reference exception: value cannot be null” is not. Write messages like you’re talking to a colleague.
- Validate on submit, not just on blur. Users will skip through fields fast. Always run a full validation pass when the submit button is clicked.
- Use Fluent UI’s
errorMessageprop when you can — it handles the red text, icon, and spacing for you. No custom CSS needed. - Reuse validation functions. If you’re validating email format in three places, extract that regex into a shared utility file rather than copy-pasting it everywhere.
Conclusion
One last thing about property pane validation specifically — when you’re in non-reactive property pane mode, and a field has a validation error, the Apply button is automatically disabled. Your users literally can’t save a broken config. That’s a free UX win from the framework; make use of it. I hope you found this article helpful!
Also, you may like:
- Upload File to SharePoint Document Library With Metadata in SPFx
- Build a Custom Slides Manager in SPFx Web Part Property Pane (Drag & Drop, Reorder, Hide Slides)
- Build a SharePoint Folder Tree View Using SharePoint Framework (SPFx)
- Display SharePoint List Items in SPFx Web Part (Complete Tutorial)

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.