Skip to content
IzzyBlazor 0.1.0-alpha.17

Enter all results ↑ ↓ move Esc close

CheckboxGroup

Several choices as checkboxes.

An interactive page: C# runs over a Blazor Server connection.

Usage

razor
@* An enum's values. *@
<CheckboxGroup @bind-Value="Model!.Toppings" Items="Enum.GetValues<Topping>()" />

@* Strings, in a row. *@
<CheckboxGroup @bind-Value="Model!.Sizes" Items="AllSizes" Orientation="Orientation.Horizontal" />

For a long list, a MultiSelect takes less room; for one yes-or-no choice, use a Checkbox.

Several choices

Bind @bind-Value to a collection and pass the options to Items: each checked box posts its value, so it works with JavaScript off.

Preview

Toppings

Code

Toppings.razor
@using Microsoft.AspNetCore.Http

<Form Model="Model" FormName="checkbox-group-toppings" OnValidSubmit="Save" Grow>
    <CheckboxGroup @bind-Value="Model!.Toppings" Items="Enum.GetValues<Topping>()" ItemText="ToppingText" Orientation="Orientation.Horizontal" />
    <Row Gap="3" Align="Align.Center" Wrap>
        <Button Submit>Add to order</Button>
        @if (_saved)
        {
            <TextBlock Size="TextSize.Sm" Color="Color.Muted">
                Added: @(Model!.Toppings.Count == 0 ? "a plain pizza" : string.Join(", ", Model.Toppings.Select(ToppingText)))
            </TextBlock>
        }
    </Row>
</Form>

@code {
    [SupplyParameterFromForm(FormName = "checkbox-group-toppings")]
    private Pizza? Model { get; set; }

    private bool _saved;

    [CascadingParameter] private HttpContext? HttpContext { get; set; }

    // A post with no box checked sends nothing for the group, 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"] == "checkbox-group-toppings";
        Model ??= posted ? new() : new() { Toppings = [Topping.Cheese] };
    }

    private void Save() => _saved = true;

    private static string ToppingText(Topping topping) => topping == Topping.ExtraSauce ? "Extra sauce" : topping.ToString();

    public sealed class Pizza
    {
        public List<Topping> Toppings { get; set; } = [];
    }

    public enum Topping
    {
        Cheese,
        Mushrooms,
        Olives,
        Peppers,
        ExtraSauce,
    }
}

The collection can be an array, a List<T>, a HashSet<T> or an interface they implement, and for an enum, Items takes its values (Enum.GetValues<Topping>()). With no box checked the group 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).

Descriptions

Use ItemDescription for a line under each option's name. Screen readers hear it after the name, as the box's description. ItemTemplate takes markup for the name itself, such as a badge beside it, and all of it names the box.

Preview

Send alerts by

Code

Descriptions.razor
@using System.ComponentModel.DataAnnotations

<Form Model="Model" FormName="checkbox-group-descriptions" MaxWidth="28.Rem" Grow>
    <CheckboxGroup @bind-Value="Model!.Channels" Items="Enum.GetValues<Channel>()"
                   ItemText="channel => Details[channel].Name" ItemDescription="channel => Details[channel].Detail" />
</Form>

@code {
    [SupplyParameterFromForm(FormName = "checkbox-group-descriptions")]
    private Alerts? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private static readonly Dictionary<Channel, (string Name, string Detail)> Details = new()
    {
        [Channel.Email] = ("Email", "To ada@example.com, within a minute."),
        [Channel.Sms] = ("SMS", "To your phone, for failed deploys only."),
        [Channel.Slack] = ("Slack", "To #deploys in the Acme workspace."),
    };

    public sealed class Alerts
    {
        [Display(Name = "Send alerts by")]
        public HashSet<Channel> Channels { get; set; } = [Channel.Email, Channel.Slack];
    }

    public enum Channel { Email, Sms, Slack }
}

Select all

Use SelectAll for a box above the options that checks or clears them all at once, and SelectAllText for its text ("Select all" by default).

Preview

Notify me about

Code

Everything.razor
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Http

<Form Model="Model" FormName="checkbox-group-everything" OnValidSubmit="Save" MaxWidth="24.Rem" Grow>
    <CheckboxGroup @bind-Value="Model!.Topics" Items="Enum.GetValues<Topic>()" SelectAll SelectAllText="Everything" />
    <Row Gap="3" Align="Align.Center" Wrap>
        <Button Submit>Save</Button>
        @if (_saved is not null)
        {
            <TextBlock Size="TextSize.Sm" Color="Color.Muted">Last saved: @_saved</TextBlock>
        }
    </Row>
</Form>

@code {
    [SupplyParameterFromForm(FormName = "checkbox-group-everything")]
    private Notifications? Model { get; set; }

    private string? _saved;

    [CascadingParameter] private HttpContext? HttpContext { get; set; }

    // With nothing checked the group posts nothing: give the defaults only when this form didn't post.
    protected override void OnInitialized()
    {
        var posted = HttpContext?.Request.HasFormContentType == true
            && HttpContext.Request.Form["_handler"] == "checkbox-group-everything";
        Model ??= posted ? new() : new() { Topics = [Topic.Comments] };
    }

    // What was saved, not the field: on an interactive page it changes as boxes are checked.
    private void Save() => _saved = Model!.Topics is { Count: > 0 } topics
        ? string.Join(", ", Enum.GetValues<Topic>().Where(topics.Contains)) + "."
        : "nothing.";

    public sealed class Notifications
    {
        [Display(Name = "Notify me about")]
        public List<Topic>? Topics { get; set; } = [];
    }

    public enum Topic { Comments, Mentions, Releases }
}

The box doesn't post: the options do, as before. It works through the script on static pages, so with JavaScript off it isn't shown and people check each option. On an interactive page, ValueChanged runs once for each option it flips, so code that saves on every change sees the steps between.

At least one

Use [Required] and [MinLength(1)] together to need at least one checked box.

Preview

Days you can work (required)

Choose at least one.

Code

AtLeastOne.razor
@using System.ComponentModel.DataAnnotations

<Form Model="Model" FormName="checkbox-group-at-least-one" OnValidSubmit="Save" OnInvalidSubmit="() => _saved = null" MaxWidth="32.Rem" Grow>
    <CheckboxGroup @bind-Value="Model!.Days" Items="Weekdays" Orientation="Orientation.Horizontal"
                   Hint="Choose at least one." />
    <Row><Button Submit>Save</Button></Row>
    @if (_saved is { Length: > 0 })
    {
        <Alert Status="Status.Success">Saved: @_saved.</Alert>
    }
    else if (_saved is not null)
    {
        <Alert Status="Status.Danger">Choose days from the list.</Alert>
    }
</Form>

@code {
    [SupplyParameterFromForm(FormName = "checkbox-group-at-least-one")]
    private Availability? Model { get; set; }

    private string? _saved;

    protected override void OnInitialized() => Model ??= new();

    // What was saved, not the field: on an interactive page it changes as boxes are checked.
    // Posted values can be anything: keep the weekdays, once each.
    private void Save() => _saved = string.Join(", ", Weekdays.Where(Model!.Days!.Contains));

    public sealed class Availability
    {
        // [Required] catches an empty choice on a static page, where nothing posts and the list
        // is null; [MinLength] on an interactive page, where it's an empty list.
        [Required(ErrorMessage = "Choose at least one day")]
        [MinLength(1, ErrorMessage = "Choose at least one day")]
        [Display(Name = "Days you can work")]
        public List<DayOfWeek>? Days { get; set; }
    }

    private static readonly DayOfWeek[] Weekdays =
    [
        DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday,
    ];
}

Live: Save checks it without a post, and the message comes and goes as boxes are checked and cleared.

The browser can't require one of several boxes, so the validator checks it: when the form posts on a static page, where an empty choice leaves the property null for [Required], and on Save or as the last box is cleared on an interactive page, where it's an empty list for [MinLength(1)]. Screen readers don't hear the asterisk; they hear "(required)" after the label instead (IzzyText.Required). After a post that fails, the group's first box takes focus, so its error is read. 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.

Sizes and disabled

Use Size to line up with buttons and other fields, and Disabled for choices that can't change, which don't post. There's no ReadOnly: HTML has no read-only checkbox, and in a read-only Form the group is disabled while a hidden field posts its value.

Preview

Small
Large
On sale

Disabled: not posted.

Code

States.razor
<Form Model="Model" FormName="checkbox-group-states" MaxWidth="32.Rem" Grow>
    <CheckboxGroup @bind-Value="Model!.Small" Items="Sizes" Label="Small" Size="ControlSize.Sm" Orientation="Orientation.Horizontal" />
    <CheckboxGroup @bind-Value="Model!.Large" Items="Sizes" Label="Large" Size="ControlSize.Lg" Orientation="Orientation.Horizontal" />
    <CheckboxGroup @bind-Value="Model!.OnSale" Items="Sizes" Label="On sale" Disabled Orientation="Orientation.Horizontal"
                   Hint="Disabled: not posted." />
</Form>

@code {
    [SupplyParameterFromForm(FormName = "checkbox-group-states")]
    private Product? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private static readonly string[] Sizes = ["S", "M", "L", "XL"];

    public sealed class Product
    {
        public string[] Small { get; set; } = ["M"];
        public string[] Large { get; set; } = ["M"];
        public string[] OnSale { get; set; } = ["XL"];
    }
}

A disabled field posts nothing, so after a static post its property holds its default (null): keep its value on the server rather than saving it from the post.

Mode support

What works in each mode, CheckboxGroup
FeatureOffStaticLive
EverythingPartlyYesYes

Off: JavaScript disabled. Static: a static SSR page with IzzyBlazor's script. Live: an interactive page (Server, WebAssembly or Auto). With JavaScript off, the SelectAll box isn't shown.

Accessibility

  • It renders native checkboxes in a fieldset whose legend is the label; each box is a Tab stop, toggled with Space.
  • Each option's text is its label; an ItemDescription is the box's description, not part of its name. The hint and errors are linked to the group with aria-describedby, and each box sets aria-invalid when the field is invalid.
  • After a post that fails with an error here, the first option takes focus, so screen readers read the group's name and error (its description); a FormErrors earlier in the page takes it first. Not on interactive pages: there, use a FormErrors.
  • A required group's boxes can't be marked required (HTML can't require one of several), so screen readers hear "(required)" after its label instead (IzzyText.Required); the asterisk is hidden from them. Nothing stops a post with none checked: the server's validation reports it.
  • A SelectAll box is named by its text, and the options under it are a group with the same name, so screen readers hear them as its part. It's announced as "mixed" when only some are checked. Its text should say it stands for all of them ("Select all", "All channels").

Parameters

CheckboxGroup parameters
PropTypeDefault
ItemsThe options.IEnumerable<TItem>?Required
ItemTextThe text for each option.Func<TItem, string>?computed
ItemTemplateMarkup for each option's label, such as an icon or a badge beside its name.RenderFragment<TItem>?
ItemDescriptionA line under each option's name, such as what choosing it does.Func<TItem, string?>?
OrientationStack options vertically (the default) or in a row.OrientationOrientation.Vertical
SelectAllAdds a box above the options that checks or clears them all at once and shows a dash when only some are checked.boolfalse
SelectAllTextThe SelectAll box's text.string?computed
@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