CheckboxGroup
A static page with IzzyBlazor's script. No circuit.
Usage
@* 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
Code
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>
{
[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
Code
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>
{
[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
Code
System.ComponentModel.DataAnnotations
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>
{
[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
Code
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>
{
[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,
];
}Static: Saving with no box checked shows the message under the group without a reload; it stays until the next Save.
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
Code
<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>
{
[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
| Feature | Off | Static | Live |
|---|---|---|---|
| Everything | Partly | Yes | Yes |
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
fieldsetwhoselegendis the label; each box is a Tab stop, toggled with Space. - Each option's text is its
label; anItemDescriptionis the box's description, not part of its name. The hint and errors are linked to the group witharia-describedby, and each box setsaria-invalidwhen 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
FormErrorsearlier in the page takes it first. Not on interactive pages: there, use aFormErrors. - 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
SelectAllbox 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
| Prop | Type | Default |
|---|---|---|
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. | Orientation | Orientation.Vertical |
SelectAllAdds a box above the options that checks or clears them all at once and shows a dash when only some are checked. | bool | false |
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? |
Plus 26 field props.
Full APISee also
- MultiSelect: several choices as chips you can type to filter.
- Checkbox: a checkbox drawn to match the fields.
- RadioGroup: one choice from a list of radios.
- Troubleshooting: An empty selection comes back with its defaults.