Ribbon
The desktop ribbon: a strip of tabs over a body of titled command groups, in the shape Office made the convention for applications with more commands than a toolbar can hold. Tabs, groups, large and small buttons, toggles, split and menu buttons, contextual tabs, a quick access row, a collapsing body, and groups that fold themselves into buttons when the window gets narrow.
Two packages ship the same shape:
Shiny.Maui.Controls.Desktop— MAUI desktop (Windows, macOS AppKit, Mac Catalyst, Linux), in theShiny.Maui.Controls.Desktop.Ribbonsnamespace.Shiny.Blazor.Controls— Blazor. There is no Blazor add-on package: the ribbon ships in the main controls package, the same split docking uses.
Neither host needs any registration — the ribbon is markup, not a service.
Screenshots
Section titled “Screenshots”Blazor
| Expanded, Home tab | Insert tab | Narrow window |
|---|---|---|
![]() |
![]() |
![]() |
MAUI (iOS)
| Groups folded into buttons | A contextual tab |
|---|---|
![]() |
![]() |
The shape
Section titled “The shape”Ribbon└── RibbonTab "Home", "Insert", "Review" — one row of the strip └── RibbonGroup "Clipboard", "Font" — a titled box of related commands └── items buttons, toggles, split/menu buttons, separators, your own contentEverything is authored declaratively: nested elements on MAUI, nested components on Blazor.
.NET MAUI
Section titled “.NET MAUI”dotnet add package Shiny.Maui.Controls.Desktopxmlns:shiny="http://shiny.net/maui/controls" — the same prefix as the core controls; the ribbon is
mapped onto it from the Desktop assembly, so a XAML author never has to know there are two assemblies
involved.
<shiny:Ribbon ApplicationButtonText="File" ApplicationButtonCommand="{Binding OpenFileMenu}" DisplayMode="{Binding DisplayMode}">
<shiny:Ribbon.QuickAccessItems> <shiny:RibbonButton Text="Save" Size="Small" Icon="save.png" Command="{Binding Save}" /> <shiny:RibbonButton Text="Undo" Size="Small" Icon="undo.png" Command="{Binding Undo}" /> </shiny:Ribbon.QuickAccessItems>
<shiny:RibbonTab Title="Home" Key="home">
<shiny:RibbonGroup Title="Clipboard" Priority="30"> <shiny:RibbonSplitButton Text="Paste" Icon="paste.png" Command="{Binding Paste}"> <shiny:RibbonMenuEntry Text="Keep source formatting" Command="{Binding PasteKeep}" /> <shiny:RibbonMenuEntry Text="Text only" Command="{Binding PasteText}" /> <shiny:RibbonMenuEntry IsSeparator="True" /> <shiny:RibbonMenuEntry Text="Paste special…" Command="{Binding PasteSpecial}" /> </shiny:RibbonSplitButton>
<shiny:RibbonButton Text="Cut" Size="Small" Icon="cut.png" Command="{Binding Cut}" /> <shiny:RibbonButton Text="Copy" Size="Small" Icon="copy.png" Command="{Binding Copy}" /> </shiny:RibbonGroup>
<shiny:RibbonGroup Title="Font" ShowDialogLauncher="True" DialogLauncherCommand="{Binding OpenFontDialog}"> <shiny:RibbonToggleButton Text="Bold" Size="Small" Icon="bold.png" IsChecked="{Binding Bold}" /> <shiny:RibbonToggleButton Text="Italic" Size="Small" Icon="italic.png" IsChecked="{Binding Italic}" />
<shiny:RibbonSeparator />
<!-- any view at all can sit in a group --> <shiny:RibbonContentItem Size="Small"> <shiny:FontSizePicker SelectedFontSize="{Binding FontSize}" WidthRequest="86" /> </shiny:RibbonContentItem> </shiny:RibbonGroup> </shiny:RibbonTab></shiny:Ribbon>Blazor
Section titled “Blazor”dotnet add package Shiny.Blazor.Controls@using Shiny.Blazor.Controls
<Ribbon ApplicationButtonText="File" ApplicationButtonClicked="OpenFileMenu" @bind-DisplayMode="mode" @bind-SelectedKey="tab">
<QuickAccess> <RibbonButton Size="RibbonItemSize.Small" Text="Save" Icon="@SaveSvg" Clicked="Save" /> </QuickAccess>
<ChildContent> <RibbonTab Title="Home" Key="home"> <RibbonGroup Title="Clipboard" Priority="30"> <RibbonSplitButton Text="Paste" Icon="@PasteSvg" Clicked="Paste" Menu="@pasteMenu" /> <RibbonButton Size="RibbonItemSize.Small" Text="Cut" Icon="@CutSvg" Clicked="Cut" /> </RibbonGroup>
<RibbonGroup Title="Font" ShowDialogLauncher="true" DialogLauncherClicked="OpenFontDialog"> <RibbonToggleButton Size="RibbonItemSize.Small" Text="Bold" Icon="@BoldSvg" @bind-Checked="bold" /> <RibbonSeparator /> <RibbonContent Size="RibbonItemSize.Small"> <select @bind="fontSize">…</select> </RibbonContent> </RibbonGroup> </RibbonTab> </ChildContent></Ribbon>
@code { List<RibbonMenuEntry> pasteMenu = new() { new() { Text = "Keep source formatting", OnClick = … }, new() { IsSeparator = true }, new() { Text = "Text only", OnClick = … } };}Two Razor-specific rules: Size is a qualified enum (Size="RibbonItemSize.Small"), and once you use
<QuickAccess> you must name <ChildContent> too — Blazor’s rule for a component with more than one
RenderFragment, not the ribbon’s.
Columns fall out of the sizes
Section titled “Columns fall out of the sizes”Nothing declares a column. A Large item takes one to itself — icon over label, the shape a primary
command wants — and Small items stack up to SmallItemRows (three, the ribbon convention) deep in a
shared column. A RibbonSeparator, or a large item, ends the current column and starts a fresh one.
┌──────────┬──────────────────┬─────────┐│ │ Cut │ │ Paste = Large → its own column│ Paste │ Copy │ Bullets │ Cut/Copy/Format = three Small → one column│ ▾ │ Format painter │ │ Bullets = Large → the next column└──────────┴──────────────────┴─────────┘That is the whole of a ribbon’s layout language, and it is why reordering a group’s items re-flows it
with nothing else touched. On MAUI the columns are built in code; on Blazor they are a CSS grid using
grid-auto-flow: column, which fills a column top to bottom before moving across.
Item kinds
Section titled “Item kinds”| Kind | What it is |
|---|---|
RibbonButton |
A plain command. Command/CommandParameter and Clicked on MAUI, Clicked on Blazor |
RibbonToggleButton |
Stays pressed. IsChecked (MAUI) / Checked (Blazor) is two-way — bind it and skip the handler |
RibbonSplitButton |
Face runs the default action, chevron opens the dropdown |
RibbonMenuButton |
The whole face opens the dropdown; no default action |
RibbonSeparator |
A full-height rule, and a break in the column flow |
RibbonContentItem (MAUI) / RibbonContent (Blazor) |
Hosts arbitrary content — a picker, a combo, a swatch strip |
Every item carries Text, Icon, Tooltip, Description, Size and enabled/visible flags. Bind the
enabled flag rather than removing an item: a command that disappears when it cannot run makes the bar
move under the pointer. A group can dim its whole contents in one place without every item having to be
bound.
Dropdown entries are RibbonMenuEntry — Text, Icon, IsChecked (draws a tick), IsSeparator, and
nestable Children that fly out as a submenu. They are markup children on MAUI and a
List<RibbonMenuEntry> on Blazor, the same split ToolbarItem.Children already uses: XAML has no
comfortable way to write a nested object graph inline, and Razor does.
- MAUI —
Iconis anImageSource, so a PNG, an SVG or aFontImageSourceglyph all work. For an icon that is drawn rather than loaded, setIconTemplateto aDataTemplatereturning a view; it wins overIconand is instantiated per drawn button, so it must not return a shared instance. - Blazor —
Iconis a string: inline SVG markup, an image URL, or a glyph. Exactly what aToolbarItemtakes, so one icon convention covers both controls.
Contextual tabs
Section titled “Contextual tabs”Nothing special declares one. Setting ContextTitle captions the coloured band above the strip and
marks the tab contextual; binding its visibility to whatever the tab is about is what makes it come
and go.
<shiny:RibbonTab Title="Format" Key="picture" ContextTitle="Picture Tools" IsVisible="{Binding PictureSelected}"><RibbonTab Title="Format" Key="picture" ContextTitle="Picture Tools" Visible="@pictureSelected">When the showing tab stops being selectable — hidden, disabled or removed — the ribbon falls back to
the nearest tab that still is, so a vanished selection never leaves an empty body. Both hosts report
that as a Fallback reason on their tab-changed event.
A contextual tab underlines in ContextColor (the theme’s tertiary by default) rather than the accent
the permanent tabs use, so it reads as a different kind of thing rather than the selected one of the
same kind.
Collapsing the ribbon
Section titled “Collapsing the ribbon”DisplayMode is two-way on both hosts:
| Mode | |
|---|---|
Expanded |
Tab strip plus the open body. The default |
Collapsed |
Only the strip. Picking a tab peeks the body back; the next command puts it away again |
Simplified |
One dense row — every item drawn small, group titles dropped |
The chevron at the trailing end of the strip toggles Expanded ⇄ Collapsed, and so does a second click
on the tab already showing. AllowCollapse="false" removes both gestures; DisplayMode still works
from code.
Groups that do not fit
Section titled “Groups that do not fit”When the showing tab is wider than the bar, groups fold into a single button that opens the whole group
in a popup — lowest Priority first, rightmost breaking ties. Items are never dropped individually,
because half a group is worse than a closed one.
Raise Priority on the groups that should survive longest, and set CanCollapse="false" on one that
must stay open. AllowGroupCollapse="false" turns the behaviour off and lets the body scroll
horizontally instead, which is the better answer when every group is small.
The decision needs real measured widths, so on Blazor it is made in the ribbon’s JS module and handed back to the component. It degrades cleanly: with no module — prerendering, a locked-down host — every group stays open and the body scrolls.
Theming
Section titled “Theming”Both hosts follow the Shiny theme with no configuration: MAUI resolves ShinyThemeKeys.Color.* through
dynamic resources, Blazor through the --shiny-color-* custom properties, and both flip with
light/dark. AccentColor, HeaderBackgroundColor and BodyBackgroundColor override just those three;
everything else stays on the theme.
Platform notes
Section titled “Platform notes”- Dropdowns are drawn above the page rather than inside the bar, which is what stops a menu being clipped by a body only three rows tall. On Blazor that is the browser’s top layer via the popover API, with Escape and a click-away both closing the panel; on MAUI it is the shared page overlay.
- macOS AppKit (
net10.0-macos) gives no native view to a child added after the page has been laid out. The ribbon is built for that: every tab’s body is created up front and switched withIsVisible, so tab switching, collapsing and group folding all work. Adding a tab, group or item after the fact does rebuild, and the dropdown panels are added on demand — those are the two paths that head cannot follow. Every other desktop target is unaffected. - Collapsed group buttons fall back to the first item’s icon on MAUI; on Blazor set
CollapsedIconexplicitly, since a group’s items are components it cannot read an icon out of. - Key tips — the Alt-key letter badges — are not implemented on either host.







