Skip to main content
twblazor
Home
  • Get started
  • Tailwind with Blazor
  • Why twblazor?
  • Theme
  • Class Merge
    • Configuring localization
    • RTL Provider
  • Breadcrumb
  • Card
  • Carousel
  • Collapse
  • Footer
  • MenuNew
  • Navbar
  • On This PageNew
  • Sidebar
  • Stepper
  • Tabs
  • Tree List
  • Button
  • FormNew
    • AutocompleteNew
    • Checkbox
    • Color Picker
    • Date Picker
    • Date Range Picker
    • Datetime Picker
    • Datetime Range Picker
    • File Upload
    • NumericNew
    • Radio Button
    • RatingNew
    • Select
    • Slider
    • Switch
    • Textfield
    • Time Picker
    • Time Range Picker
  • Alert
  • Avatar
  • Chip
  • Icon
  • Progress
  • Skeleton
  • Spinner
  • TooltipNew
  • Data Table
  • Pagination
  • Pick List
  • BarcodeNew
  • QR CodeNew
  • CalendarNew
  • Table
  • AttachmentNew
  • Overview
    • Bar
    • Bullet
    • Column
    • Dot Plot
    • Dumbbell
    • Icon
    • Lollipop
    • Radial Bar
    • Range
    • Word Cloud
    • Area
    • Bump
    • Bump Area
    • Candlestick
    • Gantt
    • Horizon
    • Line
    • Slope
    • Small Multiples
    • Stream Graph
    • Funnel
    • Gauge
    • Marimekko
    • Nightingale
    • Pie
    • Treemap
    • Waffle
    • Waterfall
    • Circular Treemap
    • Convex Treemap
    • Dendrogram
    • Sunburst
    • Barcode
    • Beeswarm
    • Box Plot
    • Density Plot
    • Histogram
    • Radial Histogram
    • Ridgeline
    • Strip Plot
    • Violin Plot
    • Categorical Scatter
    • Contour
    • Hexagonal Binning
    • Quadrant
    • Scatter
    • Heatmap
    • Matrix
    • Parallel Coordinates
    • Radar
    • Arc Diagram
    • Chord Diagram
    • Flowchart
    • Network Diagram
    • Sankey
    • Euler Diagram
    • Venn Diagram
  • Bubble
  • Marker
  • Message
  • Dialog Service
  • Toast Service
API DocumentationAI Agents & Skills
Home Get Started

Forms

TwForm

TwForm is a Blazor form that validates fields with data annotations and validators, shows an error summary and focuses the first error.

Last updated 21 October 2026

Please be aware this component is new, may have bugs and/or be subject to change. Please report any bugs at https://github.com/twblazor/twblazor/issues.

Validating a model

Give TwForm a Model and bind each field to it with @bind-Value. The model's data annotations, such as [Required], are checked when the form is submitted and again as each field changes. A field with an error shows its message, and is marked invalid for screen readers. OnValidSubmit only runs once every field is valid.
RAZOR
<TwForm Model="@signup" OnValidSubmit="@SaveSignup">
    <TwTextfield T="string" Label="Name" @bind-Value="signup.Name" />
    <TwTextfield T="string" Label="Email" InputType="email" @bind-Value="signup.Email" />
    <TwSelect T="string" Label="Plan" Values="@plans" @bind-SelectedValue="signup.Plan" Placeholder="Choose a plan" />
    <TwCheckbox T="bool" Label="I agree to the terms" @bind-Value="signup.AcceptTerms" />
    <div>
        <TwButton Type="submit" Variant="ButtonVariant.Filled" Color="Color.Primary" Label="Sign up" />
    </div>
    @if (signupSaved)
    {
        <p role="status" class="text-sm text-green-800 dark:text-green-300">Thanks, @signup.Name. You are signed up.</p>
    }
</TwForm>
CSHARP
@code {
    private readonly SignupModel signup = new();
    private readonly string[] plans = ["Free", "Pro", "Team"];
    private bool signupSaved;
    
    private void SaveSignup() => signupSaved = true;
    
    private sealed class SignupModel
    {
        [Required(ErrorMessage = "Enter your name")]
        public string? Name { get; set; }
    
        [Required(ErrorMessage = "Enter your email address")]
        [EmailAddress(ErrorMessage = "Enter an email address in the correct format, like [email protected]")]
        public string? Email { get; set; }
    
        [Required(ErrorMessage = "Choose a plan")]
        public string? Plan { get; set; }
    
        [Range(typeof(bool), "true", "true", ErrorMessage = "Agree to the terms to continue")]
        public bool AcceptTerms { get; set; }
    }
}

Error summary

Set ShowSummary to list every error above the fields after a failed submit. Focus moves to the summary, so screen readers read it out, and each error is a link that moves focus to its field. Without a summary, focus moves to the first invalid field instead. Set FocusOnError="false" to leave focus where it is.
RAZOR
<TwForm Model="@address" ShowSummary>
    <TwTextfield T="string" Label="Street" @bind-Value="address.Street" />
    <TwTextfield T="string" Label="Town or city" @bind-Value="address.Town" />
    <TwTextfield T="string" Label="Postcode" @bind-Value="address.Postcode" />
    <div>
        <TwButton Type="submit" Variant="ButtonVariant.Filled" Color="Color.Primary" Label="Save address" />
    </div>
</TwForm>
CSHARP
@code {
    private readonly AddressModel address = new();
    
    private sealed class AddressModel
    {
        [Required(ErrorMessage = "Enter the street")]
        public string? Street { get; set; }
    
        [Required(ErrorMessage = "Enter the town or city")]
        public string? Town { get; set; }
    
        [Required(ErrorMessage = "Enter a postcode")]
        [RegularExpression(@"^[A-Za-z]{1,2}\d[A-Za-z\d]? ?\d[A-Za-z]{2}$", ErrorMessage = "Enter a real postcode, like SW1A 1AA")]
        public string? Postcode { get; set; }
    }
}

Field validators

A field's own Validator runs as it changes and again when the form is submitted, so it can stop the form submitting too. Use it for rules that don't belong on the model, or that need to look something up.
RAZOR
<TwForm Model="@account" OnValidSubmit="@(() => accountSaved = true)">
    <TwTextfield T="string" Label="Username" @bind-Value="account.Username" Validator="@CheckUsernameAsync" />
    <TwNumeric T="int" Label="Age" @bind-Value="account.Age" />
    <div>
        <TwButton Type="submit" Variant="ButtonVariant.Filled" Color="Color.Primary" Label="Create account" />
    </div>
    @if (accountSaved)
    {
        <p role="status" class="text-sm text-green-800 dark:text-green-300">Account created.</p>
    }
</TwForm>
CSHARP
@code {
    private readonly AccountModel account = new();
    private bool accountSaved;
    private static readonly string[] _takenUsernames = ["admin", "root", "twblazor"];
    
    private static Task<string?> CheckUsernameAsync(string? username) =>
        Task.FromResult(username is not null && _takenUsernames.Contains(username.Trim().ToLowerInvariant())
            ? "That username is taken. Try another."
            : null);
    
    private sealed class AccountModel
    {
        [Required(ErrorMessage = "Enter a username")]
        [StringLength(20, MinimumLength = 3, ErrorMessage = "Usernames are 3 to 20 characters long")]
        public string? Username { get; set; }
    
        [Range(18, 120, ErrorMessage = "You must be 18 or over")]
        public int Age { get; set; }
    }
}

Choices, dates and numbers

Every TwBlazor input takes part in validation, including radio groups, switches, sliders, ratings, and the date, time and color pickers. Bind each one with its own value parameter, for example @bind-SelectedDate on a date picker.
Room
RAZOR
<TwForm Model="@booking">
    <TwRadioGroup T="string" Legend="Room" Items="@rooms" @bind-Value="booking.Room" />
    <TwDatePicker Label="Arrival date" @bind-Value="booking.Arrival" />
    <TwNumeric T="int" Label="Guests" Min="1" Max="6" @bind-Value="booking.Guests" />
    <TwSwitch T="bool" Label="Breakfast included" @bind-Value="booking.Breakfast" />
    <div>
        <TwButton Type="submit" Variant="ButtonVariant.Filled" Color="Color.Primary" Label="Book" />
    </div>
</TwForm>
CSHARP
@code {
    private readonly BookingModel booking = new();
    private readonly List<TwBlazor.Models.RadioGroupItem<string>> rooms =
    [
        new() { Label = "Single", Value = "single" },
        new() { Label = "Double", Value = "double" },
        new() { Label = "Suite", Value = "suite" }
    ];
    
    private sealed class BookingModel
    {
        [Required(ErrorMessage = "Choose a room")]
        public string? Room { get; set; }
    
        [Required(ErrorMessage = "Enter your arrival date")]
        public string? Arrival { get; set; }
    
        [Range(1, 6, ErrorMessage = "Rooms take 1 to 6 guests")]
        public int Guests { get; set; } = 1;
    
        public bool Breakfast { get; set; }
    }
}

Validating from code

Call ValidateAsync to check the form without submitting it, for example before moving to the next step of a wizard. IsValid holds the last result and IsModified says whether anything has changed.
RAZOR
<TwForm @ref="@contactForm" Model="@contact">
    <TwTextfield T="string" Label="Phone number" InputType="tel" @bind-Value="contact.Phone" />
    <div class="flex items-center gap-3">
        <TwButton Label="Check" Variant="ButtonVariant.Filled" Color="Color.Primary" OnClick="@CheckContactAsync" />
        <span role="status" class="text-sm">@contactStatus</span>
    </div>
</TwForm>
CSHARP
@code {
    private TwForm? contactForm;
    private readonly ContactModel contact = new();
    private string contactStatus = string.Empty;
    
    private async Task CheckContactAsync()
    {
        var valid = contactForm is not null && await contactForm.ValidateAsync();
        contactStatus = valid ? "Looks good." : "Fix the phone number first.";
    }
    
    private sealed class ContactModel
    {
        [Required(ErrorMessage = "Enter a phone number")]
        [Phone(ErrorMessage = "Enter a phone number, like 07700 900 982")]
        public string? Phone { get; set; }
    }
}

In an EditForm

TwBlazor fields also work in Blazor's own EditForm, with DataAnnotationsValidator or any other validator. They show the form's messages and report every change, just as they do in a TwForm.
RAZOR
<EditForm Model="@newsletter" class="flex flex-col gap-4">
    <DataAnnotationsValidator />
    <TwTextfield T="string" Label="Email" InputType="email" @bind-Value="newsletter.Email" />
    <div>
        <TwButton Type="submit" Variant="ButtonVariant.Filled" Color="Color.Primary" Label="Subscribe" />
    </div>
</EditForm>
CSHARP
@code {
    private readonly NewsletterModel newsletter = new();
    
    private sealed class NewsletterModel
    {
        [Required(ErrorMessage = "Enter your email address")]
        [EmailAddress(ErrorMessage = "Enter an email address in the correct format, like [email protected]")]
        public string? Email { get; set; }
    }
}

Accessibility

The browser's own validation bubbles are turned off, because screen readers don't reliably announce them. Instead each invalid field is marked with aria-invalid and its message is linked with aria-describedby and announced as it appears. After a failed submit, focus moves to the error summary or the first invalid field, so nobody is left on the submit button wondering what happened. Each summary error links to its field. Give every field a Label, and use Legend on radio groups.

Keyboard navigation

A form is not a Tab stop itself. Its fields keep their own keyboard controls. After a failed submit, focus moves to the error summary, or to the first invalid field when there is no summary.
Key What it does
Tab or Shift+ plus Tab Moves between the fields, the submit button and the links in the error summary.
Enter Submits the form from a text field or the submit button. On a link in the error summary, moves focus to that field.

Theme configuration

Styling is controlled by TwBlazorTheme.Components (a TwFormTheme), configured via the AddTwBlazor() options callback described on the Get Started page. Every property is a set of Tailwind classes, so changes are picked up by your own Tailwind build.
Property Type Controls
Base string
Summary string
SummaryTitle string
SummaryList string
SummaryLink string

Full reference: TwFormTheme on the API docs.

On this page

  • Validating a model
  • Error summary
  • Field validators
  • Choices, dates and numbers
  • Validating from code
  • In an EditForm
  • Accessibility
  • Keyboard navigation
  • Theme configuration
Discord (opens in a new tab) • GitHub (opens in a new tab) • Documentation (opens in a new tab) • MIT License (opens in a new tab) • llms.txt (opens in a new tab)

twblazor is and always will be free and open source.

An unhandled error has occurred. Reload