Loading
Four ways to show that something is on its way. Which one to reach for depends on what you know and what is already on screen.
Choosing one
Nothing on screen yet, and you know the shape of what is coming —
VetoSkeleton. It holds the layout so nothing jumps when the data lands.
Content already on screen, being refreshed —
VetoLoadingOverlay. Replacing what someone is reading with placeholders
takes away what they had.
You can say how far along it is — VetoProgress. A bar
that fills tells someone how long to wait; a spinner only tells them to wait.
Small, inline, and the duration is unknown —
VetoSpinner. Inside a button, beside a label.
Spinner
Decorative by default. Give it a Label only when it is the sole sign that something is happening - beside text that already says so, a second announcement is noise.
<!-- Decorative: something nearby already says what is happening -->
<VetoSpinner />
<!-- The only sign: give it a name -->
<VetoSpinner Label="Checking availability" />Progress
Determinate when a total is known, indeterminate when it is not. Both report themselves as a progress bar, so the value is announced rather than only drawn.
<VetoProgress Value="_percent" Label="Uploading" ShowValue="true" />
<VetoProgress Indeterminate="true" Label="Contacting the server" />
<VetoButton OnClick="More">Add 20%</VetoButton>
@code {
private double _percent = 40;
private void More() => _percent = Math.Min(100, _percent + 20);
}A ring instead of a bar
For a figure that has to sit inside something small and square - a tile, an avatar, a button - where a bar would be too short to read or would force the layout wider. The arc grows by offsetting a dash rather than by redrawing the path, because an SVG path's d cannot be transitioned and redrawing it would jump. An indeterminate ring turns rather than fills: there is no figure to show, so showing one would be a lie.
<VetoProgress Shape="VetoProgressShape.Circle" Value="@_progress" ShowValue="true" />
<VetoProgress Shape="VetoProgressShape.Circle" Value="@_progress" Thickness="4" />
<VetoProgress Shape="VetoProgressShape.Circle" Indeterminate="true" />Game meters - health, mana, experience
Segments notch the bar into pips, and a part-filled pip still reads as part-filled because the notches are drawn over one fill rather than the bar being split into separate elements. Trail is the damage bar: hold the old value there and it is left behind when the health drops, which is what makes a hit readable - a bar that simply shortens says the number changed, where one that leaves a bright remainder says how much was lost.
@* Notched pips, and a damage bar left behind when the health drops. *@
<VetoProgress Label="Health" Value="@_health" Trail="@_healthTrail"
Segments="10" Color="VetoColor.Danger" Rounded="false"
ShowValue="true" ValueSuffix=" HP" />
<VetoProgress Label="Experience" Value="@_xp" Color="VetoColor.Warning"
Size="VetoSize.Small" ShowValue="true" ValueFormat="0.0" />A download, counted in its own units
ValueContent prints whatever the number actually means rather than a percentage of it - bytes transferred, files done, time left. It is handed the component, so it can read Value, Max and Percent and say something true about all three. ValueFormat and ValueSuffix cover the simpler case where a percentage is right but its shape is not.
<VetoProgress Label="veto-1.0.0.zip" Value="@_progress" ShowValue="true">
<ValueContent Context="bar">
@($"{bar.Value * 0.84:0.0} MB of 84 MB")
</ValueContent>
</VetoProgress>
<VetoProgress Value="@_progress" ShowValue="true" ValueFormat="0.0" ValueSuffix="% done" />Something riding the bar
Runner is content carried along the leading edge of the fill - a car on a road, a jet climbing away. It rides its own rail outside the track, which clips, so a runner taller than the bar is not cut off at the knees. It is handed the component too, so it can change as it goes: the jet raises its nose the further it gets.
@* Carried along the leading edge, and handed the bar so it can change as it goes. *@
<VetoProgress Label="Take-off" Value="@_progress" ShowValue="true">
<Runner Context="bar">
<span style="@($"transform: rotate({-bar.Percent * 0.35:0.#}deg)")">✈</span>
</Runner>
</VetoProgress>Skeleton
Placeholders in the shape of the content that replaces them. The last line of a multi-line block is short, the way a real paragraph ends. Hidden from assistive technology - announce the wait once on the region, not once per grey box.
<VetoSkeleton Variant="VetoSkeletonVariant.Circle" />
<VetoSkeleton Lines="3" />
<VetoSkeleton Variant="VetoSkeletonVariant.Rectangle" Width="12rem" />Loading overlay
For a refresh, where the old content should stay put. It is marked inert while the veil is up, so a control that is about to be replaced cannot be clicked or tabbed into behind it.
<VetoLoadingOverlay Loading="_refreshing" Label="Refreshing">
<VetoCard Title="Team">…</VetoCard>
</VetoLoadingOverlay>
<VetoButton OnClick="RefreshAsync">Refresh</VetoButton>
@code {
private bool _refreshing;
private async Task RefreshAsync()
{
_refreshing = true;
await LoadAsync();
_refreshing = false;
}
}No flash for a fast refresh
ShowDelay holds the veil back for that many milliseconds. A refresh that finishes sooner never shows it - no flicker for a fast response - while a slow one still gets the veil. The content is inert from the first moment either way.
<VetoLoadingOverlay Loading="_refreshing" ShowDelay="400" Label="Refreshing">
…
</VetoLoadingOverlay>Reduced motion
All four respect prefers-reduced-motion, and none of them simply stop.
A still spinner reads as a broken ring, so it pulses instead; the indeterminate bar
fills the track and pulses rather than sweeping across it; the skeleton keeps its
placeholders and drops only the shimmer. Something that is working should still look
like it is working.
VetoSpinner
| Parameter | Type | Default | Description |
|---|---|---|---|
Size |
VetoSize |
Medium |
ExtraSmall, Small, Medium, Large or ExtraLarge. |
Color |
VetoColor? |
null |
Inherits the surrounding text colour when null. |
Label |
string? |
null |
Accessible name. Null keeps the spinner decorative and silent. |
Thickness |
string |
"2px" |
The stroke's thickness. |
VetoProgress
| Parameter | Type | Default | Description |
|---|---|---|---|
Value |
double |
0 |
How far along the work is, from 0 to Max. |
Max |
double |
100 |
The value that counts as finished. |
Indeterminate |
bool |
false |
Animates continuously instead of filling. |
Label |
string? |
null |
Text above the bar. |
ShowValue |
bool |
false |
Shows the percentage beside the label. |
Size |
VetoSize |
Medium |
The bar's thickness. |
Color |
VetoColor |
Primary |
The colour role. |
Rounded |
bool |
true |
Rounds the ends of the bar. |
AccessibleLabel |
string |
"Progress" |
Name used when no Label is given. |
VetoSkeleton
| Parameter | Type | Default | Description |
|---|---|---|---|
Variant |
VetoSkeletonVariant |
Text |
Text, Rectangle or Circle. |
Lines |
int |
1 |
How many lines to draw. Text only. |
Width / Height |
string? |
null |
CSS lengths. Text and circles size themselves. |
Animated |
bool |
true |
Whether the shimmer runs. |
VetoLoadingOverlay
| Parameter | Type | Default | Description |
|---|---|---|---|
Loading |
bool |
false |
Whether the veil is showing. |
Label |
string |
"Loading" |
What is being waited for. Announced, and shown beside the spinner. |
Indicator |
RenderFragment? |
null |
Replaces the default spinner and label. |
Dim |
bool |
true |
Whether the content is dimmed as well as veiled. |
ShowDelay |
int |
0 |
Milliseconds before the veil fades in. A shorter refresh never shows it; the content is inert from the start. |