Skip to content
Shiny.Net.HttpServer v1 - A lightweight feature rich HTTP Server - Tunnels, Websockets, AOT, ASPNET Featureset, & Works EVERYWHERE!Let me see!

ShinyButton | Blazor Usage

The Blazor <ShinyButton> mirrors the MAUI control parameter-for-parameter and renders a real <button type="button">, so keyboard activation, the focus ring and disabled all come from the platform.

Two differences the platforms force:

  • There is no ICommand on the web, so the command-state integration is MAUI-only. Its equivalent here is that Clicked is awaited — an async handler holds the button busy for exactly as long as it runs.
  • Motion icons default to currentColor, so they inherit the button’s CSS color including the hover and disabled states with nothing to wire up. Do not set IconColor unless you want to break that.
@using Shiny.Blazor.Controls
<ShinyButton Text="Save" BusyText="Saving..." LeftMotionIcon="download" Clicked="SaveAsync" />
<ShinyButton Text="Delete"
Appearance="ButtonAppearance.Outlined"
Type="ButtonType.Critical"
LeftMotionIcon="trash"
Clicked="DeleteAsync" />
<ShinyButton Text="Cancel" Appearance="ButtonAppearance.Text" Clicked="@(() => open = false)" />
@code {
bool open = true;
// Async: the button is busy for the whole call, then returns to normal.
async Task SaveAsync() => await http.PostAsJsonAsync("/api/save", model);
async Task DeleteAsync() => await http.DeleteAsync($"/api/items/{id}");
}

A synchronous handler never produces a spinner flicker — the returned task is checked for completion before any state change, so a handler that finished inline is treated as instant.

<ShinyButton Text="Submit"
@bind-State="submitState"
BusyText="Submitting..."
SuccessText="Submitted"
ErrorText="Failed"
StateRevertDelay="@TimeSpan.FromSeconds(2)"
Clicked="SubmitAsync" />
@code {
ButtonState submitState = ButtonState.Normal;
async Task SubmitAsync()
{
await http.PostAsJsonAsync("/api/submit", model);
submitState = ButtonState.Success; // the button respects this, rather than resetting
}
}

@bind-State and @bind-IsBusy both work. The component tracks the rendered state separately from its parameters and only lets a parameter the parent actually changed override it — which is what lets an async handler hold the button busy while the parent keeps re-supplying the same IsBusy="false" on every render.

A handler that throws sets Error (when ShowErrorOnFault is set) and the exception is still rethrown, so your error boundary and logging see it.

Everything from the MAUI surface, with these Blazor forms:

Parameter Type Default Notes
Text string? null
TextColor string? null CSS colour
FontSize double 15 px
FontFamily string? null Unset inherits from the page
FontWeight string? "500" CSS font-weight
Appearance ButtonAppearance Filled
Type ButtonType Primary
ButtonBackgroundColor string? null CSS colour; wins over the tokens
BorderColor string? null CSS colour
BorderThickness double -1 px; -1 lets the appearance decide
CornerRadius double 10 px
ContentPadding string "10px 16px" A CSS padding value, not a Thickness
HasShadow bool? null Null lets the appearance decide
FullWidth bool false Stretches to the container
LeftIcon / RightIcon string? null An image URL or raw SVG/HTML markup
LeftMotionIcon / RightMotionIcon string? null A motion icon name
LeftIconContent / RightIconContent RenderFragment? null The LeftIconView equivalent
IconSize double 20 px
IconColor string? null Unset leaves motion icons on currentColor
IconSpacing double 8 px
ContentLayout ButtonContentLayout Sides
State / StateChanged ButtonState Normal @bind-State
IsBusy / IsBusyChanged bool false @bind-IsBusy
BusyMode ButtonBusyMode ReplaceLeftIcon
BusyText / SuccessText / ErrorText string? null Null keeps Text
BusyMotionIcon string? "loader" Cleared falls back to a CSS spinner
BusyContent RenderFragment? null Wins over BusyMotionIcon
SuccessMotionIcon / ErrorMotionIcon string? "check" / "warning"
StateRevertDelay TimeSpan 1.5s Zero holds
DisableWhileBusy bool true
AutoBusy bool true Hold busy for an awaited Clicked
ShowErrorOnFault bool true
Clicked EventCallback<MouseEventArgs> Awaited
Disabled bool false The IsEnabled equivalent
CssClass string? null Extra classes on the button element

Anything else is splatted onto the <button>, so aria-label, id, data-*, title and form all work:

<ShinyButton LeftMotionIcon="settings"
CornerRadius="22"
ContentPadding="11px"
aria-label="Settings"
Clicked="OpenSettings" />

Colours come from the --shiny-color-* custom properties via shiny-btn--{appearance} and shiny-btn--{type} classes rather than inline styles, so the theme carries through and a theme switch needs no re-render. A type class sets four custom properties (base, on-base, container, on-container) and the appearance class decides which of them to paint with — which is also how an explicit colour parameter wins, since it overrides the same custom property inline with no specificity fight.

Hover and press are a filter: brightness() for the filled appearances and a color-mix() state layer for the transparent ones, so they work over any background — including a colour you supplied yourself, which no token rule could have anticipated.

prefers-reduced-motion is honoured: transitions are dropped and the fallback spinner slows down.

<!-- Default: spinner takes the left icon's place, text stays, width cannot change -->
<ShinyButton Text="Save" LeftMotionIcon="download" BusyText="Saving..." Clicked="SaveAsync" />
<!-- Content fades but keeps its layout space, so the button holds its width -->
<ShinyButton Text="Save" BusyMode="ButtonBusyMode.ReplaceContent" Clicked="SaveAsync" />
<!-- Spinner alongside the existing content -->
<ShinyButton Text="Save" BusyMode="ButtonBusyMode.KeepContent" Clicked="SaveAsync" />
<!-- Your own indicator -->
<ShinyButton Text="Save" Clicked="SaveAsync">
<BusyContent>
<MyCustomSpinner />
</BusyContent>
</ShinyButton>