SharePoint Framework (SPFx) Form Validation: A Complete Guide with Examples

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
}
SPFx Form Validation

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;
}
}
form validation in spfx

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.

SPFx web part form validation using React Form

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 errorMessage prop on TextField

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.

SPFx web part form validation using react states

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 empty
  • minLength / maxLength — character limits
  • pattern — regex matching
  • validate — custom function returning true or an error string

Custom validation with validate:

rules={{
validate: (value) => {
if (value.includes('test')) {
return 'Project name cannot contain the word "test".';
}
return true;
}
}}
validate spfx react hook form

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.

form validation in spfx using formik and Yup

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 Controller plays 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']}
PnP DynamicForm Control validation in spfx

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

ScenarioBest Approach
Property pane text/number fieldsonGetErrorMessage (built-in)
Simple 2-3 field custom formManual useState validation
Complex multi-field React formReact Hook Form + Fluent UI
Forms with cross-field date/value rulesFormik + Yup
CRUD form for a SharePoint listPnP DynamicForm control
Validate against SharePoint APIAsync 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 errorMessage prop 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:

Power Apps functions free pdf

30 Power Apps Functions

This free guide walks you through the 30 most-used Power Apps functions with real business examples, exact syntax, and results you can see.

Download User registration canvas app

DOWNLOAD USER REGISTRATION POWER APPS CANVAS APP

Download a fully functional Power Apps Canvas App (with Power Automate): User Registration App