MultiSelect
A static page with IzzyBlazor's script. No circuit.
Usage
@* Strings: each is its own text. *@
<MultiSelect @bind-Value="Model!.Skills" Items="AllSkills" Placeholder="Add skills" />
@* Ids, named for people. *@
<MultiSelect @bind-Value="Model!.ReviewerIds" Items="People.Keys"
ItemText="id => People.GetValueOrDefault(id, id.ToString())" />
@* An enum's values. *@
<MultiSelect @bind-Value="Model!.Days" Items="Enum.GetValues<DayOfWeek>()" />For a short list, a CheckboxGroup shows every option at once; for one choice, use a Combobox or a Select.
Choosing several
Bind @bind-Value to a collection and pass the options to Items: chosen options show as chips, typing filters the list, ignoring case and accents, and the list stays open as people choose.
Preview
Code
System.ComponentModel.DataAnnotations
Microsoft.AspNetCore.Http
<Form Model="Model" FormName="multi-select-chips" OnValidSubmit="Save" MaxWidth="28.Rem" Grow>
<MultiSelect @bind-Value="Model!.Skills" Items="AllSkills" Placeholder="Add skills" Hint="Up to 5." />
<Row Gap="3" Align="Align.Center" Wrap>
<Button Submit>Save</Button>
if (_saved)
{
<TextBlock Size="TextSize.Sm" Color="Color.Muted">Saved: (Model!.Skills is { Length: > 0 } skills ? string.Join(", ", skills) : "no skills")</TextBlock>
}
</Row>
</Form>
{
[SupplyParameterFromForm(FormName = "multi-select-chips")]
private Profile? Model { get; set; }
private bool _saved;
[CascadingParameter] private HttpContext? HttpContext { get; set; }
// A post with nothing chosen sends nothing for the field, so the model isn't filled from it:
// give the defaults only when this form didn't post.
protected override void OnInitialized()
{
// This form's own post: another form on the page posts too.
var posted = HttpContext?.Request.HasFormContentType == true
&& HttpContext.Request.Form["_handler"] == "multi-select-chips";
Model ??= posted ? new() : new() { Skills = ["Blazor", "C#"] };
}
private void Save() => _saved = true;
public sealed class Profile
{
[MaxLength(5, ErrorMessage = "Pick up to 5 skills.")]
public string[] Skills { get; set; } = [];
}
}Static: Chosen skills show as chips, typing filters the list, choosing keeps it open, and Backspace removes the last chip.
The collection can be an array, a List<T>, a HashSet<T> or an interface they implement. Each choice posts as a repeated form field, and an empty choice posts nothing: on a static page the property is then null, or the model isn't bound at all, so set defaults only when the request isn't this form's post, as the example does (why). [MaxLength] limits how many, checked by the validator, not the browser.
At least one, with a placeholder
Use [Required] and [MinLength(1)] together to need at least one choice, and ItemText to name options whose values are ids.
Preview
Code
System.ComponentModel.DataAnnotations
<Form Model="Model" FormName="multi-select-reviewers" OnValidSubmit="Save" MaxWidth="28.Rem" Grow>
<MultiSelect @bind-Value="Model!.ReviewerIds" Items="People.Keys"
ItemText="id => People.GetValueOrDefault(id, id.ToString())"
Placeholder="Add reviewers" />
<Row><Button Submit>Request review</Button></Row>
if (_requested is { Count: > 0 })
{
<Alert Status="Status.Success">Review requested from string.Join(", ", _requested).</Alert>
}
else if (_requested is not null)
{
<Alert Status="Status.Danger">Choose reviewers from the list.</Alert>
}
</Form>
{
[SupplyParameterFromForm(FormName = "multi-select-reviewers")]
private ReviewRequest? Model { get; set; }
private List<string>? _requested;
protected override void OnInitialized() => Model ??= new();
// Posted values can be anything: keep the people, once each.
private void Save() => _requested = Model!.ReviewerIds!.Distinct().Where(People.ContainsKey).Select(id => People[id]).ToList();
public sealed class ReviewRequest
{
// [Required] catches an empty choice on a static page, where it posts nothing and the
// list is null; [MinLength] on an interactive page, where it's an empty list.
[Required(ErrorMessage = "Add at least one reviewer")]
[MinLength(1, ErrorMessage = "Add at least one reviewer")]
[Display(Name = "Reviewers")]
public List<int>? ReviewerIds { get; set; }
}
}Static: The browser stops the post until someone is chosen, in the model's words, on the field.
The browser checks [Required] before posting. On the server, [Required] catches an empty choice on a static page, where the property is null, and [MinLength(1)] catches it on an interactive page after the last choice is removed, when it's an empty list. A post can carry values that aren't among the options, and they reach the model, so check them on the server, as the example does.
Enum options with icons
Use ItemTemplate to add markup to each option in the list, such as an icon, and pass an enum's values to Items (Enum.GetValues<IssueLabel>()).
Preview
Code
<Form Model="Model" FormName="multi-select-enum-icons" MaxWidth="24.Rem" Grow>
<MultiSelect @bind-Value="Model!.Labels" Items="Enum.GetValues<IssueLabel>()" Placeholder="Add labels">
<ItemTemplate Context="label">
<Icon Name="LabelIcon(label)" />
<span>label</span>
</ItemTemplate>
</MultiSelect>
</Form>
{
[SupplyParameterFromForm(FormName = "multi-select-enum-icons")]
private Issue? Model { get; set; }
protected override void OnInitialized() => Model ??= new();
private static IconData LabelIcon(IssueLabel label) => label switch
{
IssueLabel.Bug => Lucide.Bug,
IssueLabel.Feature => Lucide.Sparkles,
IssueLabel.Docs => Lucide.BookOpen,
_ => Lucide.Gauge,
};
public sealed class Issue
{
public List<IssueLabel> Labels { get; set; } = [IssueLabel.Bug];
}
public enum IssueLabel { Bug, Feature, Docs, Performance }
}Chips, typing and the plain list with JavaScript off use the option's text, not the template: ItemText, or else the enum's [Display(Name)] or its name. The template writes its own text, so show the same there.
Sizes, read-only and disabled
Use Size to line up with buttons and other fields, ReadOnly for choices that post but can't change, and Disabled for ones that don't post.
Preview
Code
<Form Model="Model" FormName="multi-select-states" MaxWidth="24.Rem" Grow>
<MultiSelect @bind-Value="Model!.Small" Items="Days" Label="Small" Size="ControlSize.Sm" />
<MultiSelect @bind-Value="Model!.Large" Items="Days" Label="Large" Size="ControlSize.Lg" />
<MultiSelect @bind-Value="Model!.Office" Items="Days" Label="Office days"
ReadOnly Hint="Read-only: posted, but can't be changed." />
<MultiSelect @bind-Value="Model!.OnCall" Items="Days" Label="On-call days"
Disabled Hint="Disabled: not posted." />
</Form>
{
[SupplyParameterFromForm(FormName = "multi-select-states")]
private Schedule? Model { get; set; }
protected override void OnInitialized() => Model ??= new();
public sealed class Schedule
{
public DayOfWeek[] Small { get; set; } = [DayOfWeek.Monday];
public DayOfWeek[] Large { get; set; } = [DayOfWeek.Monday, DayOfWeek.Friday];
public DayOfWeek[] Office { get; set; } = [DayOfWeek.Tuesday, DayOfWeek.Thursday];
public DayOfWeek[] OnCall { get; set; } = [DayOfWeek.Saturday];
}
}HTML has no read-only select, so a read-only MultiSelect shows its choices in a read-only text field and posts them from hidden inputs.
Its own words
Use EmptyText for "No matches", ToggleLabel for the name of the button that opens the list and RemoveLabel for the start of each chip's remove button name ("Remove Blazor"), or set them for the whole app as in Text and other languages.
Mode support
| Feature | Off | Static | Live |
|---|---|---|---|
| Choose several options, posted with the form | Yes | Yes | Yes |
| Chips, and typing to filter, ignoring case and accents | No | Yes | Yes |
| Markup in each option (ItemTemplate) | No | Yes | Yes |
| @bind-Value follows each choice | No | No | Yes |
Off: JavaScript disabled. Static: a static SSR page with IzzyBlazor's script. Live: an interactive page (Server, WebAssembly or Auto).
Accessibility
- With script, the input is a
comboboxwith a multi-selectlistbox: chosen options havearia-selected, and focus stays in the input as Down and Up move through them. - Enter toggles the highlighted option and the list stays open; Escape or Tab closes it.
- Backspace in the empty input removes the last chosen option. The chips' remove buttons, named "Remove" and the option ("Remove Design",
RemoveLabel), are left out of the Tab order. - When nothing matches, "No matches" (
EmptyText) is announced politely. - The label names the input and the list. The chosen options are the input's description, before the hint and errors ("Chosen: Design, Writing.",
IzzyText.Chosen), so they're heard on focus. - With JavaScript off, it's a native multiple
select, with the browser's own keys.
Parameters
| Prop | Type | Default |
|---|---|---|
ReadOnlyMakes the field read-only: the chosen options show in a read-only text field, and the values still post. | bool | false |
ItemsThe options. | IEnumerable<TItem>? | Required |
ItemTextThe text for each option, also what's typed to find it and the chip's text. | Func<TItem, string>? | computed |
ItemTemplateMarkup for each option in the list, such as an icon. | RenderFragment<TItem>? | |
PlaceholderText in the empty field, such as "Add skills". | string? | |
EmptyTextShown when nothing matches. | string? | |
ToggleLabelThe accessible name of the button that opens the list. | string? | |
RemoveLabelThe start of each chip's remove button name for screen readers, before the option's text. | string? | "Remove" |
@bind-ValueThe value, bound with @bind-Value. | TValue | |
DisplayNameThe field's name in validation messages from the input itself, such as a value that can't be parsed. | string? |
Plus 26 field props.
Full APISee also
- CheckboxGroup: several choices as checkboxes.
- Combobox: type to filter; server search on interactive pages.
- Select: a native select, themed where the browser allows.
- Troubleshooting: An empty selection comes back with its defaults.