Skip to content
IzzyBlazor 0.1.0-alpha.17

Enter all results ↑ ↓ move Esc close

MultiSelect

Several choices as chips you can type to filter.

A static page with IzzyBlazor's script. No circuit.

Usage

razor
@* 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

BlazorC#
Accessibility
ASP.NET Core
Azure
Blazor
C#
CSS
Docker
Entity Framework
F#
Git
GraphQL
HTML
JavaScript
Kubernetes
Playwright
PostgreSQL
React
Rust
SignalR
SQL Server
TypeScript
Vue

Up to 5.

Code

Chips.razor
@using System.ComponentModel.DataAnnotations
@using 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>

@code {
    [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

Ada Lovelace
Alan Turing
Barbara Liskov
Edsger Dijkstra
Grace Hopper
José Valim
Katherine Johnson
Linus Torvalds
Margaret Hamilton
Zoë Morgan

Code

Reviewers.razor
@using 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>

@code {
    [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

Bug
Bug
Feature
Docs
Performance

Code

EnumIcons.razor
<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>

@code {
    [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

Monday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
MondayFriday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday

Read-only: posted, but can't be changed.

Saturday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday

Disabled: not posted.

Code

States.razor
<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>

@code {
    [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

What works in each mode, MultiSelect
FeatureOffStaticLive
Choose several options, posted with the formYesYesYes
Chips, and typing to filter, ignoring case and accentsNoYesYes
Markup in each option (ItemTemplate)NoYesYes
@bind-Value follows each choiceNoNoYes

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 combobox with a multi-select listbox: chosen options have aria-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

MultiSelect parameters
PropTypeDefault
ReadOnlyMakes the field read-only: the chosen options show in a read-only text field, and the values still post.boolfalse
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?

See also