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:

razor
<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:

ContactModel.cs
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:

You can use either OnValidSubmit/OnInvalidSubmit together, or OnSubmit alone, but not both patterns simultaneously.

razor
<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:

CustomValidator.cs
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:

razor
<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:

razor
<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:

style.css
.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.