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

ButtonGroup

Joins related buttons into one segmented control — inner corners square off and adjacent outlines collapse into a single edge, running across or down.

  • NuGet downloads for Shiny.Maui.Controls
  • NuGet downloads for Shiny.Blazor.Controls
Frameworks
.NET MAUI
Blazor
  • One control out of several buttons — the joining is automatic: the outer corners keep the theme’s radius, the inner ones square off, and adjacent outlines are pulled together into one hairline.
  • Horizontal or verticalOrientation turns the whole thing, rounding and collapse included.
  • Optional selectionSelectionMode is None by default. Turn it on and the same markup becomes a segmented picker with a two-way SelectedIndex.
  • Split buttonsButtonGroupSeparator divides a primary action from its alternatives.
  • Static segmentsButtonGroupText is a label, unit or prefix sitting flush with the buttons.
  • Clusters — a group of groups keeps each inner group’s merged edges and spaces the two apart.
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:ButtonGroup>
<shiny:ShinyButton Text="Archive" Appearance="Outlined" Command="{Binding ArchiveCommand}" />
<shiny:ShinyButton Text="Report" Appearance="Outlined" Command="{Binding ReportCommand}" />
<shiny:ShinyButton Text="Snooze" Appearance="Outlined" Command="{Binding SnoozeCommand}" />
</shiny:ButtonGroup>
<ButtonGroup>
<ShinyButton Text="Archive" Appearance="ButtonAppearance.Outlined" Clicked="ArchiveAsync" />
<ShinyButton Text="Report" Appearance="ButtonAppearance.Outlined" Clicked="ReportAsync" />
<ShinyButton Text="Snooze" Appearance="ButtonAppearance.Outlined" Clicked="SnoozeAsync" />
</ButtonGroup>

SelectionMode defaults to None, and with it the group touches nothing — it joins the segments and gets out of the way, with each button keeping its own command. Turn it on and the same markup becomes a picker:

<shiny:ButtonGroup SelectionMode="Single" SelectedIndex="{Binding RangeIndex}">
<shiny:ShinyButton Text="Day" />
<shiny:ShinyButton Text="Week" />
<shiny:ShinyButton Text="Month" />
</shiny:ButtonGroup>
<ButtonGroup SelectionMode="ButtonGroupSelectionMode.Single" @bind-SelectedIndex="range">
<ShinyButton Text="Day" />
<ShinyButton Text="Week" />
<ShinyButton Text="Month" />
</ButtonGroup>

While a selection mode is on, the group owns each segment’s appearance — selected segments take SelectedAppearance (Filled), the rest UnselectedAppearance (Outlined) — unless the segment set an appearance of its own, which wins, so one odd segment in a picker stays odd. Leaving the selection mode hands every appearance back.

Single ignores a click on the already-selected segment, because a picker that can be emptied by clicking its own answer again is a picker with no answer; AllowDeselect allows it. Multiple always toggles. Only buttons are selectable — a text segment or a separator is chrome, and counting it would make the indexes a view model sees depend on where a divider happens to sit.

Reach for TabbedPage’s ShinyTabBar instead when the segments switch pages rather than a value.

<shiny:ButtonGroup>
<shiny:ShinyButton Text="Follow" Type="Secondary" Command="{Binding FollowCommand}" />
<shiny:ButtonGroupSeparator />
<shiny:ShinyButton RightMotionIcon="chevron-down" Type="Secondary"
SemanticProperties.Description="More follow options"
Command="{Binding MoreCommand}" />
</shiny:ButtonGroup>
<shiny:ButtonGroup>
<shiny:ButtonGroupText Text="USD" />
<shiny:ShinyButton Text="−" Appearance="Outlined" Command="{Binding DecreaseCommand}" />
<shiny:ShinyButton Text="+" Appearance="Outlined" Command="{Binding IncreaseCommand}" />
</shiny:ButtonGroup>
<ButtonGroup>
<ShinyButton Text="Follow" Type="ButtonType.Secondary" Clicked="FollowAsync" />
<ButtonGroupSeparator />
<ShinyButton RightMotionIcon="chevron-down" Type="ButtonType.Secondary"
aria-label="More follow options" Clicked="MoreAsync" />
</ButtonGroup>
<ButtonGroup>
<ButtonGroupText Text="USD" />
<ShinyButton Text="−" Appearance="ButtonAppearance.Outlined" Clicked="DecreaseAsync" />
<ShinyButton Text="+" Appearance="ButtonAppearance.Outlined" Clicked="IncreaseAsync" />
</ButtonGroup>

A separator belongs between two filled segments. Outlined ones already have an edge, and the collapsed outline is the divider.

Property Type Default Description
Orientation StackOrientation / ToolbarOrientation Horizontal Which way the segments run
SelectionMode ButtonGroupSelectionMode None None, Single or Multiple
SelectedIndex int -1 The selected segment (two-way)
SelectedIndexes IReadOnlyList<int> empty Every selected index, ascending
AllowDeselect bool false Whether the selected segment can clear itself in Single
SelectedAppearance ButtonAppearance Filled How a selected segment paints
UnselectedAppearance ButtonAppearance Outlined How an unselected segment paints
CollapseBorders bool true Pull each outlined segment into the one before it
CornerRadius double unset The outer corners; unset follows the theme
BorderThickness double unset How much of an overlap the collapse takes (MAUI)
ClusterSpacing double 8 Gap between nested groups (MAUI)

Events: SelectionChanged, plus SelectionChangedCommand on MAUI.

Worth knowing, because it is where the hosts genuinely differ.

On MAUI the group resolves the theme’s corner radius and hairline thickness numerically — through a hidden probe in its own resource chain, so a live theme swap re-runs the geometry rather than freezing it at startup — and pushes squared-off corners onto each segment. The collapse is a negative margin, and it is applied only to segments that actually paint an outline: a filled segment has no edge to double up, and pulling it into its neighbour would only overlap the two fills.

On Blazor the joining is pure CSS over :first-child / :last-child using logical corner properties, so it mirrors under a right-to-left reading direction with no second rule, and it works for any segment without the group counting its children. One consequence to know: a per-button CornerRadius is written as an inline custom property, and inline wins — that button keeps its own rounding inside a group.