Skip to content
Shiny.NET
Shiny MAUI Shell v7 - App Links, App Shortcuts, & Navigation Interception!Shortcut me to it

ChipGroup

A bound set of chips: one per item in ItemsSource, selectable singly or in any number, reported back through SelectedItem and SelectedItems.

  • NuGet downloads for Shiny.Maui.Controls
  • NuGet downloads for Shiny.Blazor.Controls
Frameworks
.NET MAUI
Blazor
  • Bound, not written out — the chips come from a collection, so a list that arrives from a service becomes a picker with no markup per option.
  • The selection is the itemsSelectedItem and SelectedItems hand back the objects a view model already has, rather than indexes it has to translate.
  • Single, multiple or none — a choice chip, a filter chip, or an action chip that carries no state at all.
  • It wraps — a dozen filters flow onto as many lines as they need, instead of becoming one unreadably long segmented control.
  • Optional removalAllowRemove puts a ✕ on every chip, with a cancellable seam in front of it.
  • Templated labelsItemTemplate/ChipContent replaces what a chip says, never its state or its way out.
Where the options come from What the selection is
ButtonGroup written out in markup an index
ChipGroup a bound collection the items themselves
TagEntry the user types them the strings they typed

A button group’s SelectedIndex is the right shape for Day / Week / Month and the wrong one for a list that arrives from a service. Reach for a chip group whenever the options are data.

claude plugin marketplace add shinyorg/skills
claude plugin install shiny@shiny

One plugin installs all 36 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

copilot plugin marketplace add https://github.com/shinyorg/skills
copilot plugin install shiny@shiny

One plugin installs all 36 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

View Skills Repository
<shiny:ChipGroup ItemsSource="{Binding Categories}"
SelectedItem="{Binding Category}"
DisplayMemberPath="Name" />
<ChipGroup ItemsSource="@categories" @bind-SelectedItem="category" DisplaySelector="@(c => c.Name)" />

SelectionMode defaults to Single — the opposite of ButtonGroup, whose default is None. Selection is what a chip group is for; a button group is a row of actions that only becomes a picker when asked.

<!-- choice chips -->
<shiny:ChipGroup ItemsSource="{Binding Ranges}" SelectedItem="{Binding Range}" />
<!-- filter chips -->
<shiny:ChipGroup ItemsSource="{Binding Filters}"
SelectionMode="Multiple"
SelectedItems="{Binding SelectedFilters}"
MaxSelectionCount="3" />
<!-- action chips: no state at all, every tap is reported -->
<shiny:ChipGroup ItemsSource="{Binding Actions}"
SelectionMode="None"
ChipTappedCommand="{Binding RunCommand}" />
<ChipGroup ItemsSource="@ranges" @bind-SelectedItem="range" />
<ChipGroup ItemsSource="@filters"
SelectionMode="ChipSelectionMode.Multiple"
@bind-SelectedItems="selectedFilters"
MaxSelectionCount="3" />
<ChipGroup ItemsSource="@actions"
SelectionMode="ChipSelectionMode.None"
ChipTapped="@((string item) => Run(item))" />

Re-tapping the selected chip in Single does nothing unless AllowDeselect is on — a picker that can be emptied by tapping its own answer again is a picker with no answer. In Multiple a second tap always toggles.

At MaxSelectionCount a tap on an unselected chip does nothing. The oldest selection is not dropped: silently unpicking something the user chose is worse than refusing the new one.

SelectedItem and SelectedItems are both kept accurate whatever the mode, so a view model can read either. On MAUI the bound SelectedItems list is written into rather than replaced, so an ObservableCollection keeps its identity and whatever is watching it goes on working; a read-only or fixed-size list (an array) is left alone rather than throwing. Anything selected that the source no longer offers is dropped — a selection with no chip to show for it is one the user cannot see and cannot undo.

On Blazor, SelectedItem cannot express “nothing selected” for a value type, because default(TItem) is a perfectly good item. Bind SelectedItems, or make the type argument nullable (ChipGroup<int?>).

Unset, a chip shows the item’s ToString().

<shiny:ChipGroup ItemsSource="{Binding Teams}" DisplayMemberPath="Name" />
<shiny:ChipGroup ItemsSource="{Binding Teams}"
ItemDisplayBinding="{Binding Members, StringFormat='{0} people'}" />
<ChipGroup ItemsSource="@teams" DisplaySelector="@(t => t.Name)" />

ItemDisplayBinding is a binding rather than a property name looked up by reflection: the path is compiled, so it survives trimming, and a converter or a StringFormat comes along for free. It wins over DisplayMemberPath when both are set.

ItemTemplate (MAUI) and ChipContent (Blazor) replace a chip’s label. The selection check and the remove affordance are never part of the template: every chip keeps the same state and the same way out, however it is drawn.

<shiny:ChipGroup ItemsSource="{Binding Teams}" SelectionMode="Multiple">
<shiny:ChipGroup.ItemTemplate>
<DataTemplate x:DataType="local:Team">
<HorizontalStackLayout Spacing="6">
<Label Text="{Binding Name}" FontSize="14" VerticalTextAlignment="Center" />
<shiny:PillView Text="{Binding Members}" Type="Info" FontSize="10" />
</HorizontalStackLayout>
</DataTemplate>
</shiny:ChipGroup.ItemTemplate>
</shiny:ChipGroup>

AllowRemove puts a ✕ on every chip, and is off by default — most chip groups are a fixed set of filters, and a ✕ on every one of them invites the user to destroy the picker.

The item is taken out of the source when the source is a list that can be written to. When it is not — a LINQ projection, an array — the event is the only signal and the view model owns the removal, which is also how a chip that needs a server round-trip first is handled.

<shiny:ChipGroup ItemsSource="{Binding Recipients}" AllowRemove="True" ChipRemoving="OnChipRemoving" />
void OnChipRemoving(object sender, ChipRemovingEventArgs e) => e.Cancel = !CanRemove(e.Item);
<ChipGroup ItemsSource="@recipients" AllowRemove ChipRemoving="@(r => recipients.Count > 1)" />

Chips wrap by default. Wrap="False" keeps them on one line — on Blazor the row then scrolls sideways on its own; on MAUI put the group in a horizontal ScrollView, or the overflow is simply off the edge.

<ScrollView Orientation="Horizontal" HorizontalScrollBarVisibility="Never">
<shiny:ChipGroup ItemsSource="{Binding Languages}" SelectionMode="Multiple" Wrap="False" />
</ScrollView>

An unselected chip is transparent with an outline, so the page behind shows through and a group over a card does not paint its own slab. A selected chip is a filled surface — the theme’s secondary container — and draws no outline, because a hairline of the unselected colour around the fill reads as a chip that is both states at once.

Set ChipBackgroundColor or SelectedChipBackgroundColor and the chip’s ink is computed from it by WCAG luminance — the same rule PillView uses — so a brand colour stays readable.

ShowSelectionCheck is on by default: fill alone carries selection only for someone who can compare a chip with the ones beside it.

IsReadOnly/ReadOnly keeps the chips crisp and the selection visible but blocks changing it, and takes the remove affordance away. Disabling the group also dims it. Read-only is “these are the values”, disabled is “not right now”.

Property Type Default Description
ItemsSource IEnumerable / IEnumerable<TItem> null What the chips stand for
SelectionMode ChipSelectionMode Single None, Single or Multiple
SelectedItem object / TItem? null The selected item (two-way)
SelectedItems IList / IReadOnlyList<TItem> empty Everything selected (two-way)
AllowDeselect bool false A second tap clears a single-selection group
MaxSelectionCount int 0 Cap in multiple mode, or 0 for no limit
ShowSelectionCheck bool true A selected chip draws a leading check
AllowRemove bool false Every chip carries a ✕
IsReadOnly / ReadOnly bool false Chips stay crisp, selection and removal are blocked
Wrap bool true Chips flow onto as many lines as they need
ItemTemplate / ChipContent DataTemplate / RenderFragment<TItem> null Replaces a chip’s label
ChipBackgroundColor Color / string transparent An unselected chip’s fill
ChipTextColor Color / string theme An unselected chip’s ink
SelectedChipBackgroundColor Color / string theme A selected chip’s fill
SelectedChipTextColor Color / string theme A selected chip’s ink
ChipBorderColor Color / string theme An unselected chip’s outline
ChipCornerRadius / CornerRadius double theme Chip corner radius

MAUI onlyItemDisplayBinding, DisplayMemberPath, HorizontalSpacing, VerticalSpacing, DisabledOpacity.

Blazor onlyDisplaySelector, Disabled, CssClass.

MAUI Blazor
Selection moved SelectionChanged, SelectionChangedCommand SelectionChanged
A chip was tapped ChipTapped, ChipTappedCommand ChipTapped
Before removal ChipRemoving (cancellable) ChipRemoving (Func<TItem, bool>)
After removal ChipRemoved, ChipRemovedCommand ChipRemoved
Drive it from code Select, Deselect, ClearSelection, IsSelected SelectAsync, DeselectAsync, ClearSelectionAsync, TapAsync, RemoveAsync

ChipTapped fires for every tap, including one that changes nothing — a re-tap of the selected chip with AllowDeselect off, or any tap at all in None. That is the point of it: a chip that acts rather than selects has no selection change to listen for.