Veto
Components

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.

Inline, with an accessible name
razor
<!-- 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.

Uploading40%
Contacting the server
razor
<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.

40%
40%
razor
<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.

Health100 HP
Mana62 MP
Experience40.0%
razor
@* 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.

veto-1.0.0.zip33.6 MB of 84 MB
Files40.0% done
razor
<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.

Delivery40%
Take-off40%
razor
@* 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)")">&#9992;</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.

razor
<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.

Team

Ada Lovelace — 12 open Grace Hopper — 4 open
razor
<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.

Inbox

3 unread messages
razor
<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.
Something went wrong. Reload 🗙