Forms and Validation in Blazor
Forms are the bread and butter of business applications. Blazor provides a comprehensive forms system built around EditForm, data annotations, and a validation pipeline that works consistently across all render modes. Let's build forms that validate properly and give users clear feedback.
The Basics: EditForm and Model Binding
Blazor's EditForm component replaces the standard HTML <form> and provides model binding, validation, and submission handling:
<EditForm Model="contact" OnValidSubmit="HandleSubmit" FormName="contact-form">
<DataAnnotationsValidator />
<ValidationSummary />
<div>
<label>Name</label>
<InputText @bind-Value="contact.Name" />
<ValidationMessage For="() => contact.Name" />
</div>
<div>
<label>Email</label>
<InputText @bind-Value="contact.Email" />
<ValidationMessage For="() => contact.Email" />
</div>
<button type="submit">Send</button>
</EditForm>
@code {
private ContactModel contact = new();
private async Task HandleSubmit()
{
await ContactService.SaveAsync(contact);
}
}
The model class uses standard data annotations:
public class ContactModel
{
[Required(ErrorMessage = "Name is required.")]
[StringLength(100, ErrorMessage = "Name must be under 100 characters.")]
public string Name { get; set; } = "";
[Required(ErrorMessage = "Email is required.")]
[EmailAddress(ErrorMessage = "Enter a valid email address.")]
public string Email { get; set; } = "";
}
Note the FormName attribute — this is required in .NET 8 for forms that use static SSR, as it identifies the form during POST handling.
Built-in Input Components
Blazor provides typed input components that handle binding and validation integration:
| Component | HTML Element | Type |
|---|---|---|
InputText |
<input type="text"> |
string |
InputNumber |
<input type="number"> |
Numeric types |
InputDate |
<input type="date"> |
DateTime |
InputCheckbox |
<input type="checkbox"> |
bool |
InputSelect |
<select> |
Any |
InputTextArea |
<textarea> |
string |
Using these instead of raw HTML elements gives you automatic validation CSS classes (valid, invalid, modified) and proper two-way binding.
OnValidSubmit vs OnSubmit
EditForm offers three submission callbacks:
OnValidSubmit— fires only if validation passesOnInvalidSubmit— fires only if validation failsOnSubmit— fires on every submission; you handle validation yourself
You can use either OnValidSubmit/OnInvalidSubmit together, or OnSubmit alone, but not both patterns simultaneously.
<EditForm Model="model" OnSubmit="HandleSubmit" FormName="manual-form">
<DataAnnotationsValidator />
@* form fields *@
<button type="submit">Save</button>
</EditForm>
@code {
private async Task HandleSubmit(EditContext editContext)
{
if (editContext.Validate())
{
// Additional custom checks
await SaveAsync();
}
}
}
Custom Validation
For validation logic that goes beyond data annotations, create a custom validator component:
public class CustomValidator : ComponentBase
{
private ValidationMessageStore? messageStore;
[CascadingParameter]
private EditContext? CurrentEditContext { get; set; }
protected override void OnInitialized()
{
if (CurrentEditContext is null)
throw new InvalidOperationException("CustomValidator requires a cascading EditContext.");
messageStore = new ValidationMessageStore(CurrentEditContext);
CurrentEditContext.OnValidationRequested += (s, e) => messageStore.Clear();
CurrentEditContext.OnFieldChanged += (s, e) => messageStore.Clear(e.FieldIdentifier);
}
public void DisplayErrors(Dictionary<string, List<string>> errors)
{
foreach (var (field, messages) in errors)
{
foreach (var message in messages)
{
messageStore!.Add(CurrentEditContext!.Field(field), message);
}
}
CurrentEditContext!.NotifyValidationStateChanged();
}
public void ClearErrors()
{
messageStore?.Clear();
CurrentEditContext?.NotifyValidationStateChanged();
}
}
Use it to display server-side validation errors:
<EditForm Model="registration" OnValidSubmit="Register" FormName="register">
<DataAnnotationsValidator />
<CustomValidator @ref="customValidator" />
@* form fields *@
</EditForm>
@code {
private CustomValidator? customValidator;
private async Task Register()
{
var result = await AuthService.RegisterAsync(registration);
if (!result.Succeeded)
{
customValidator?.DisplayErrors(result.Errors);
}
}
}
Enhanced Form Handling in .NET 8
With static SSR, forms use standard HTTP POST rather than SignalR. Blazor handles this transparently, but you need to be aware of a few things:
<EditForm Model="model" OnValidSubmit="Save" FormName="edit-item" Enhance>
<DataAnnotationsValidator />
<AntiforgeryToken />
<InputText @bind-Value="model.Title" />
<button type="submit">Save</button>
</EditForm>
The Enhance attribute enables enhanced form handling — the form submits via fetch instead of a full page navigation, giving a smoother experience. The AntiforgeryToken is required for POST requests in static SSR.
Validation CSS Classes
Blazor automatically applies CSS classes to input components based on validation state: modified, valid, and invalid. You can style these directly:
.invalid {
border-colour: #ef4444;
}
.valid.modified {
border-colour: #22c55e;
}
This gives users immediate visual feedback without writing any additional code.
Forms in Blazor are straightforward once you understand the EditForm/EditContext pattern. Use data annotations for simple rules, custom validators for server-side errors, and the built-in input components for consistent validation integration.