Blazor Advanced Components Created: 04 Feb 2026 Updated: 04 Feb 2026

Two-Way Data Binding in Blazor: A Complete Guide

Two-way data binding is one of the most powerful features in Blazor, enabling seamless synchronization between your UI and underlying data. Unlike one-way binding where data flows in a single direction, two-way binding creates a bidirectional connection – changes in the UI automatically update the source data, and changes in the source automatically update the UI.

In this comprehensive guide, we'll explore everything you need to know about two-way data binding in Blazor, from basic syntax to creating custom components with two-way binding support.

What is Two-Way Data Binding?

Two-way data binding establishes a bidirectional link between a UI element and a data source. When either side changes, the other side automatically synchronizes.

The Data Flow

┌─────────────────────────────────────────────────────────────┐
│ TWO-WAY DATA BINDING │
├─────────────────────────────────────────────────────────────┤
│ │
│ C# Code ◄─────────────────────────────► HTML/UI │
│ (Source) Data Flows Both Ways (Target) │
│ ◄──────────────────► │
│ │
│ • Source changes → UI updates automatically │
│ • UI changes → Source updates automatically │
│ │
└─────────────────────────────────────────────────────────────┘

Key Characteristics

  1. Bidirectional Flow: Data moves in both directions
  2. Automatic Sync: No manual event handlers needed for basic cases
  3. Simplified Forms: Perfect for form inputs
  4. Reduced Boilerplate: Less code compared to one-way binding with events

The @bind Directive

The @bind directive is Blazor's primary mechanism for two-way binding.

Basic Syntax

<input @bind="propertyName" />

What @bind Actually Does

When you write:

<input @bind="username" />

Blazor expands this to:

<input value="@username"
@onchange="@((ChangeEventArgs e) => username = e.Value?.ToString())" />

So @bind is essentially shorthand for:

  1. Binding the value attribute to your property (one-way)
  2. Handling the onchange event to update your property

Two-Way Binding with Different Input Types

Text Input

<input type="text" @bind="name" />
<p>Hello, @name!</p>

@code {
private string name = "World";
}

Number Input

<input type="number" @bind="quantity" />
<p>Total: $@(quantity * price)</p>

@code {
private int quantity = 1;
private decimal price = 9.99m;
}

Date Input

<input type="date" @bind="selectedDate" />
<p>Selected: @selectedDate.ToLongDateString()</p>

@code {
private DateTime selectedDate = DateTime.Today;
}

Checkbox

<input type="checkbox" @bind="isChecked" />
<p>Checked: @isChecked</p>

@code {
private bool isChecked = false;
}

Select (Dropdown)

<select @bind="selectedCountry">
<option value="">-- Select --</option>
<option value="USA">United States</option>
<option value="UK">United Kingdom</option>
<option value="CA">Canada</option>
</select>
<p>Selected: @selectedCountry</p>

@code {
private string selectedCountry = "";
}

Textarea

<textarea @bind="description" rows="4"></textarea>
<p>Character count: @description.Length</p>

@code {
private string description = "";
}

@bind Modifiers

Blazor provides several modifiers to customize two-way binding behavior.

@bind:event - Change the Trigger Event

By default, @bind uses the onchange event, which fires when the input loses focus. You can change this using @bind:event.

@* Default: Updates on blur (onchange) *@
<input @bind="text1" />

@* Updates on every keystroke (oninput) *@
<input @bind="text2" @bind:event="oninput" />

@code {
private string text1 = "";
private string text2 = "";
}

Common events:

  1. onchange (default) - Fires when element loses focus
  2. oninput - Fires on every input (real-time)

When to use oninput:

  1. Live search/filtering
  2. Character counters
  3. Real-time validation
  4. Live previews

@bind:after - Post-Binding Callback

The @bind:after modifier runs a method after the binding completes. This is useful for triggering side effects.

<input @bind="searchTerm"
@bind:event="oninput"
@bind:after="PerformSearch" />

<ul>
@foreach (var result in searchResults)
{
<li>@result</li>
}
</ul>

@code {
private string searchTerm = "";
private List<string> searchResults = new();
private List<string> allItems = new() { "Apple", "Banana", "Cherry", "Date" };
private void PerformSearch()
{
searchResults = allItems
.Where(i => i.Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
.ToList();
}
}

Use cases for @bind:after:

  1. API calls after input changes
  2. Validation logic
  3. Updating related state
  4. Logging/analytics
  5. Triggering animations

@bind:get and @bind:set - Custom Get/Set Logic

For complete control over the binding process, use @bind:get and @bind:set.

<input @bind:get="temperatureCelsius"
@bind:set="SetTemperature" />

<p>Celsius: @temperatureCelsius°C</p>
<p>Fahrenheit: @temperatureFahrenheit°F</p>

@code {
private double temperatureCelsius = 20;
private double temperatureFahrenheit => temperatureCelsius * 9 / 5 + 32;
private void SetTemperature(double value)
{
// Custom logic when setting value
temperatureCelsius = Math.Round(value, 1);
// Could also: validate, transform, trigger events, etc.
}
}

Another example - Currency formatting:

<input @bind:get="FormattedPrice"
@bind:set="SetPrice" />

<p>Actual value: @price</p>

@code {
private decimal price = 99.99m;
private string FormattedPrice => price.ToString("C");
private void SetPrice(string value)
{
// Remove currency symbols and parse
var cleanValue = value.Replace("$", "").Replace(",", "").Trim();
if (decimal.TryParse(cleanValue, out var result))
{
price = result;
}
}
}

@bind:format - Date and Number Formatting

Use @bind:format for custom date formatting.

@* Default date format *@
<input type="date" @bind="date1" />

@* Custom format in text input *@
<input type="text" @bind="date2" @bind:format="yyyy-MM-dd" />

@* Different format *@
<input type="text" @bind="date3" @bind:format="MM/dd/yyyy" />

@code {
private DateTime date1 = DateTime.Today;
private DateTime date2 = DateTime.Today;
private DateTime date3 = DateTime.Today;
}

Supported format specifiers:

  1. Date: yyyy-MM-dd, MM/dd/yyyy, dd-MMM-yyyy, etc.
  2. DateTime: yyyy-MM-ddTHH:mm, etc.

Two-Way Binding with Components

One of Blazor's most powerful features is creating custom components with two-way binding support.

The Pattern

To enable @bind-PropertyName on your component, you need:

  1. A [Parameter] property for the value
  2. An [Parameter] EventCallback<T> named PropertyNameChanged

Basic Example: SimpleInput Component

@* SimpleInput.razor *@
<div class="mb-3">
<label class="form-label">@Label</label>
<input type="text"
class="form-control"
value="@Value"
@oninput="HandleInput" />
</div>

@code {
[Parameter]
public string Value { get; set; } = "";
[Parameter]
public EventCallback<string> ValueChanged { get; set; }
[Parameter]
public string Label { get; set; } = "Input:";
private async Task HandleInput(ChangeEventArgs e)
{
var newValue = e.Value?.ToString() ?? "";
await ValueChanged.InvokeAsync(newValue);
}
}

Usage:

<SimpleInput @bind-Value="username" Label="Username:" />
<p>Value: @username</p>

@code {
private string username = "";
}

How It Works

When you write:

<SimpleInput @bind-Value="username" />

Blazor expands this to:

<SimpleInput Value="@username"
ValueChanged="@((value) => username = value)" />

Naming Convention

The naming convention is critical:

  1. Property: Value
  2. Callback: ValueChanged (property name + "Changed")
  3. Usage: @bind-Value

You can use any property name:

  1. Property: Rating → Callback: RatingChanged → Usage: @bind-Rating
  2. Property: SelectedDate → Callback: SelectedDateChanged → Usage: @bind-SelectedDate

Advanced Example: NumericStepper Component

@* NumericStepper.razor *@
<div class="input-group" style="max-width: 200px;">
<button class="btn btn-outline-secondary"
type="button"
@onclick="Decrement"
disabled="@(Value <= Min)">
</button>
<input type="number"
class="form-control text-center"
value="@Value"
min="@Min"
max="@Max"
@onchange="HandleChange" />
<button class="btn btn-outline-secondary"
type="button"
@onclick="Increment"
disabled="@(Value >= Max)">
+
</button>
</div>

@code {
[Parameter]
public int Value { get; set; }
[Parameter]
public EventCallback<int> ValueChanged { get; set; }
[Parameter]
public int Min { get; set; } = int.MinValue;
[Parameter]
public int Max { get; set; } = int.MaxValue;
[Parameter]
public int Step { get; set; } = 1;
private async Task Increment()
{
var newValue = Math.Min(Value + Step, Max);
await ValueChanged.InvokeAsync(newValue);
}
private async Task Decrement()
{
var newValue = Math.Max(Value - Step, Min);
await ValueChanged.InvokeAsync(newValue);
}
private async Task HandleChange(ChangeEventArgs e)
{
if (int.TryParse(e.Value?.ToString(), out var newValue))
{
newValue = Math.Clamp(newValue, Min, Max);
await ValueChanged.InvokeAsync(newValue);
}
}
}

Usage:

<NumericStepper @bind-Value="quantity" Min="0" Max="100" Step="5" />
<p>Quantity: @quantity</p>

@code {
private int quantity = 10;
}

Example: Star Rating Component

@* StarRating.razor *@
<div class="star-rating">
@for (int i = 1; i <= MaxStars; i++)
{
var starIndex = i;
<span style="cursor: pointer; font-size: 1.5rem; color: @(i <= Value ? "#ffc107" : "#dee2e6");"
@onclick="@(() => SetRating(starIndex))">
@(i <= Value ? "★" : "☆")
</span>
}
</div>

@code {
[Parameter]
public int Value { get; set; }
[Parameter]
public EventCallback<int> ValueChanged { get; set; }
[Parameter]
public int MaxStars { get; set; } = 5;
private async Task SetRating(int newValue)
{
await ValueChanged.InvokeAsync(newValue);
}
}

Usage:

<StarRating @bind-Value="rating" MaxStars="5" />
<p>Rating: @rating / 5</p>

@code {
private int rating = 3;
}

Example: Toggle Switch Component

@* ToggleSwitch.razor *@
<div class="form-check form-switch">
<input class="form-check-input"
type="checkbox"
checked="@Value"
@onchange="HandleChange" />
<label class="form-check-label">@Label</label>
</div>

@code {
[Parameter]
public bool Value { get; set; }
[Parameter]
public EventCallback<bool> ValueChanged { get; set; }
[Parameter]
public string Label { get; set; } = "";
private async Task HandleChange(ChangeEventArgs e)
{
var newValue = (bool)(e.Value ?? false);
await ValueChanged.InvokeAsync(newValue);
}
}

Usage:

<ToggleSwitch @bind-Value="darkMode" Label="Dark Mode" />
<p>Dark Mode: @(darkMode ? "ON" : "OFF")</p>

@code {
private bool darkMode = false;
}

Multiple Two-Way Bindings on One Component

A component can have multiple two-way bound properties.

@* DateRangePicker.razor *@
<div class="row">
<div class="col-6">
<label>Start Date:</label>
<input type="date"
value="@StartDate.ToString("yyyy-MM-dd")"
@onchange="HandleStartChange" />
</div>
<div class="col-6">
<label>End Date:</label>
<input type="date"
value="@EndDate.ToString("yyyy-MM-dd")"
@onchange="HandleEndChange" />
</div>
</div>

@code {
[Parameter]
public DateTime StartDate { get; set; }
[Parameter]
public EventCallback<DateTime> StartDateChanged { get; set; }
[Parameter]
public DateTime EndDate { get; set; }
[Parameter]
public EventCallback<DateTime> EndDateChanged { get; set; }
private async Task HandleStartChange(ChangeEventArgs e)
{
if (DateTime.TryParse(e.Value?.ToString(), out var date))
{
await StartDateChanged.InvokeAsync(date);
}
}
private async Task HandleEndChange(ChangeEventArgs e)
{
if (DateTime.TryParse(e.Value?.ToString(), out var date))
{
await EndDateChanged.InvokeAsync(date);
}
}
}

Usage:

<DateRangePicker @bind-StartDate="start" @bind-EndDate="end" />
<p>Duration: @((end - start).Days) days</p>

@code {
private DateTime start = DateTime.Today;
private DateTime end = DateTime.Today.AddDays(7);
}

One-Way vs Two-Way Binding Comparison

Aspec tOne-Way Two-Way
Syntaxvalue="@myValue"@bind="myValue"
Data FlowSource → UISource ↔ UI
UI Updates SourceNo (needs event)Yes (automatic)
Use CaseDisplay, read-onlyForms, inputs
ControlMore explicitMore convenient
Typical Elements<span>, <div>, <p><input>, <select>, <textarea>


Share this lesson: