Skip to content
Shiny Controls v1.0 - The Ultra Control Suite for .NET MAUI & BlazorO...M...G!

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 the Shiny.Maui.Controls.Desktop.Ribbons namespace.
  • 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.

  • NuGet downloads for Shiny.Maui.Controls.Desktop
  • NuGet downloads for Shiny.Blazor.Controls
Frameworks
.NET MAUI
Blazor
Operating Systems
Windows
macOS
Linux

Blazor

Expanded, Home tab Insert tab Narrow window
The Home tab with Clipboard, Font, Paragraph and Editing groups The Insert tab with the Tables and Illustrations groups The same bar in a narrow window with low-priority groups folded into buttons

MAUI (iOS)

Groups folded into buttons A contextual tab
Every group collapsed to a single button on a phone-width window The Format tab appearing once a picture is selected
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 content

Everything is authored declaratively: nested elements on MAUI, nested components on Blazor.

Terminal window
dotnet add package Shiny.Maui.Controls.Desktop

xmlns: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>
Terminal window
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.

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.

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 RibbonMenuEntryText, 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.

  • MAUIIcon is an ImageSource, so a PNG, an SVG or a FontImageSource glyph all work. For an icon that is drawn rather than loaded, set IconTemplate to a DataTemplate returning a view; it wins over Icon and is instantiated per drawn button, so it must not return a shared instance.
  • BlazorIcon is a string: inline SVG markup, an image URL, or a glyph. Exactly what a ToolbarItem takes, so one icon convention covers both controls.

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.

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.

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.

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.

  • 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 with IsVisible, 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 CollapsedIcon explicitly, 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.