ButtonGroup
Joins related buttons into one segmented control — inner corners square off and adjacent outlines collapse into a single edge, running across or down.
Features
Section titled “Features”- 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 vertical —
Orientationturns the whole thing, rounding and collapse included. - Optional selection —
SelectionModeisNoneby default. Turn it on and the same markup becomes a segmented picker with a two-waySelectedIndex. - Split buttons —
ButtonGroupSeparatordivides a primary action from its alternatives. - Static segments —
ButtonGroupTextis 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.
AI Skill
Section titled “AI Skill”Step 1 — Add the marketplace:
claude plugin marketplace add shinyorg/skillsStep 2 — Install the plugin:
claude plugin install shiny@shinyOne 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.
Step 1 — Add the marketplace:
copilot plugin marketplace add https://github.com/shinyorg/skillsStep 2 — Install the plugin:
copilot plugin install shiny@shinyOne 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.
Quick Start
Section titled “Quick Start”<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>Selection
Section titled “Selection”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.
Split buttons and static segments
Section titled “Split buttons and static segments”<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.
Properties
Section titled “Properties”| 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.
How the joining works
Section titled “How the joining works”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.


