Author : MD TAREQ HASSAN | Updated : 2020/09/01
For WebAssembly
A simple CSS only spinner (Courtesy: https://codepen.io/Keyamoon/pen/aHxuq)
wwwroot/css/app.css
/* ---------------- Spinner ---------------- */
@font-face {
font-family: 'icomoon';
src: url("data:application/x-font-ttf;charset=utf-8;base64,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") format('truetype');
font-weight: normal;
font-style: normal;
font-display: block;
}
[class^="icon-"], [class*=" icon-"] {
font-family: 'icomoon';
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-spinner:before {
content: "\e000";
}
.icon-spinner-2:before {
content: "\e001";
}
.icon-spinner-3:before {
content: "\e002";
}
.icon-spinner-4:before {
content: "\e003";
}
.icon-spinner-5:before {
content: "\e004";
}
.icon-spinner-6:before {
content: "\e005";
}
.icon-spinner-7:before {
content: "\e006";
}
@keyframes anim-rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
.spinner {
display: inline-block;
font-size: 4em;
height: 1em;
line-height: 1;
margin: .5em;
animation: anim-rotate 2s infinite linear;
color: #1cc8ae;
text-shadow: 0 0 .25em rgba(255,255,255, .3);
}
.spinner--steps {
animation: anim-rotate 1s infinite steps(8);
}
.spinner--steps2 {
animation: anim-rotate 1s infinite steps(12);
}
wwwroot/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>BlazorApp</title>
<base href="/" />
<link href="manifest.json" rel="manifest" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
</head>
<body>
<app>
<div class="container d-flex min-vh-100">
<div class="m-auto">
<section class="talign-center bg-transparent">
<div class="spinner spinner--steps icon-spinner-6" aria-hidden="true"></div>
</section>
<h2 class="text-sm-center">loading...</h2>
</div>
</div>
</app>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
<script>navigator.serviceWorker.register('service-worker.js');</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.3/js/bootstrap.bundle.min.js"></script>
<script>
$(function () {
});
</script>
</body>
</html>
Other CSS only spinner:
See: Loading spinner using css only
A Custom Spinner
Based on
- https://github.com/tobiasahlin/SpinKit (https://tobiasahlin.com/spinkit/)
- https://github.com/EdCharbeneau/CssBuilder
- https://github.com/EdCharbeneau/BlazorPro.Spinkit
ependencies
spinkit.min.css
: https://github.com/tobiasahlin/SpinKit/blob/master/spinkit.min.cssBuilderExtensions.cs
SpinnerType.cs
SpinnerBase.cs
SpinnerContainer.razor
Circle.razor
CircleFade.razor
DefaultSpinnerChooser.razor
SpinLoader.razor
Adding spinkit.min.css
using libman
- Right click on project > Add > client library > provider: unpkg
- Type “spinner” > Add
libman.json
{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [
{
"provider": "unpkg",
"library": "spinkit@2.0.1",
"destination": "wwwroot",
"files": [
"spinkit.min.css"
]
}
]
}
Styling spinner: site.css
/* ... ... ... */
/* Spinner realted */
:root {
--sk-size: 6rem;
--sk-color: #cf4b00;
}
BuilderExtensions
Components/BuilderExtensions.cs
using System;
using System.Collections.Generic;
namespace Spinner.Server.Components
{
public struct StyleBuilder
{
private string stringBuffer;
/// <summary>
/// Creates a StyleBuilder used to define conditional in-line style used in a component. Call Build() to return the completed style as a string.
/// </summary>
/// <param name="prop"></param>
/// <param name="value"></param>
public static StyleBuilder Default(string prop, string value) => new StyleBuilder(prop, value);
/// <summary>
/// Creates a StyleBuilder used to define conditional in-line style used in a component. Call Build() to return the completed style as a string.
/// </summary>
/// <param name="prop"></param>
/// <param name="value"></param>
public static StyleBuilder Default(string style) => Empty().AddStyle(style);
/// <summary>
/// Creates a StyleBuilder used to define conditional in-line style used in a component. Call Build() to return the completed style as a string.
/// </summary>
public static StyleBuilder Empty() => new StyleBuilder();
/// <summary>
/// Creates a StyleBuilder used to define conditional in-line style used in a component. Call Build() to return the completed style as a string.
/// </summary>
/// <param name="prop"></param>
/// <param name="value"></param>
public StyleBuilder(string prop, string value) => stringBuffer = stringBuffer = $"{prop}:{value};";
/// <summary>
/// Adds a conditional in-line style to the builder with space separator and closing semicolon.
/// </summary>
/// <param name="style"></param>
public StyleBuilder AddStyle(string style) => !string.IsNullOrWhiteSpace(style) ? AddRaw($"{style};") : this;
/// <summary>
/// Adds a raw string to the builder that will be concatenated with the next style or value added to the builder.
/// </summary>
/// <param name="prop"></param>
/// <param name="value"></param>
/// <returns>StyleBuilder</returns>
private StyleBuilder AddRaw(string style)
{
stringBuffer += style;
return this;
}
/// <summary>
/// Adds a conditional in-line style to the builder with space separator and closing semicolon..
/// </summary>
/// <param name="prop"></param>
/// <param name="value">Style to add</param>
/// <returns>StyleBuilder</returns>
public StyleBuilder AddStyle(string prop, string value) => AddRaw($"{prop}:{value};");
/// <summary>
/// Adds a conditional in-line style to the builder with space separator and closing semicolon..
/// </summary>
/// <param name="prop"></param>
/// <param name="value">Style to conditionally add.</param>
/// <param name="when">Condition in which the style is added.</param>
/// <returns>StyleBuilder</returns>
public StyleBuilder AddStyle(string prop, string value, bool when = true) => when ? this.AddStyle(prop, value) : this;
/// <summary>
/// Adds a conditional in-line style to the builder with space separator and closing semicolon..
/// </summary>
/// <param name="prop"></param>
/// <param name="value">Style to conditionally add.</param>
/// <param name="when">Condition in which the style is added.</param>
/// <returns></returns>
public StyleBuilder AddStyle(string prop, Func<string> value, bool when = true) => when ? this.AddStyle(prop, value()) : this;
/// <summary>
/// Adds a conditional in-line style to the builder with space separator and closing semicolon..
/// </summary>
/// <param name="prop"></param>
/// <param name="value">Style to conditionally add.</param>
/// <param name="when">Condition in which the style is added.</param>
/// <returns>StyleBuilder</returns>
public StyleBuilder AddStyle(string prop, string value, Func<bool> when = null) => this.AddStyle(prop, value, when());
/// <summary>
/// Adds a conditional in-line style to the builder with space separator and closing semicolon..
/// </summary>
/// <param name="prop"></param>
/// <param name="value">Style to conditionally add.</param>
/// <param name="when">Condition in which the style is added.</param>
/// <returns>StyleBuilder</returns>
public StyleBuilder AddStyle(string prop, Func<string> value, Func<bool> when = null) => this.AddStyle(prop, value(), when());
/// <summary>
/// Adds a conditional nested StyleBuilder to the builder with separator and closing semicolon.
/// </summary>
/// <param name="builder">Style Builder to conditionally add.</param>
/// <returns>StyleBuilder</returns>
public StyleBuilder AddStyle(StyleBuilder builder) => this.AddRaw(builder.Build());
/// <summary>
/// Adds a conditional nested StyleBuilder to the builder with separator and closing semicolon.
/// </summary>
/// <param name="builder">Style Builder to conditionally add.</param>
/// <param name="when">Condition in which the style is added.</param>
/// <returns>StyleBuilder</returns>
public StyleBuilder AddStyle(StyleBuilder builder, bool when = true) => when ? this.AddRaw(builder.Build()) : this;
/// <summary>
/// Adds a conditional in-line style to the builder with space separator and closing semicolon..
/// </summary>
/// <param name="builder">Style Builder to conditionally add.</param>
/// <param name="when">Condition in which the styles are added.</param>
/// <returns>StyleBuilder</returns>
public StyleBuilder AddStyle(StyleBuilder builder, Func<bool> when = null) => this.AddStyle(builder, when());
/// <summary>
/// Adds a conditional in-line style to the builder with space separator and closing semicolon..
/// A ValueBuilder action defines a complex set of values for the property.
/// </summary>
/// <param name="prop"></param>
/// <param name="builder"></param>
/// <param name="when"></param>
public StyleBuilder AddStyle(string prop, Action<ValueBuilder> builder, bool when = true)
{
ValueBuilder values = new ValueBuilder();
builder(values);
return AddStyle(prop, values.ToString(), when && values.HasValue);
}
/// <summary>
/// Adds a conditional in-line style when it exists in a dictionary to the builder with separator.
/// Null safe operation.
/// </summary>
/// <param name="additionalAttributes">Additional Attribute splat parameters</param>
/// <returns>StyleBuilder</returns>
public StyleBuilder AddStyleFromAttributes(IReadOnlyDictionary<string, object> additionalAttributes) =>
additionalAttributes == null ? this :
additionalAttributes.TryGetValue("style", out var c) ? AddRaw(c.ToString()) : this;
/// <summary>
/// Finalize the completed Style as a string.
/// </summary>
/// <returns>string</returns>
public string Build()
{
// String buffer finalization code
return stringBuffer != null ? stringBuffer.Trim() : string.Empty;
}
// ToString should only and always call Build to finalize the rendered string.
public override string ToString() => Build();
}
public class ValueBuilder
{
private string stringBuffer;
public bool HasValue => !string.IsNullOrWhiteSpace(stringBuffer);
/// <summary>
/// Adds a space separated conditional value to a property.
/// </summary>
/// <param name="value"></param>
/// <param name="when"></param>
/// <returns></returns>
public ValueBuilder AddValue(string value, bool when = true) => when ? AddRaw($"{value} ") : this;
public ValueBuilder AddValue(Func<string> value, bool when = true) => when ? AddRaw($"{value()} ") : this;
private ValueBuilder AddRaw(string style)
{
stringBuffer += style;
return this;
}
public override string ToString() => stringBuffer != null ? stringBuffer.Trim() : string.Empty;
}
public struct CssBuilder
{
private string stringBuffer;
/// <summary>
/// Creates a CssBuilder used to define conditional CSS classes used in a component.
/// Call Build() to return the completed CSS Classes as a string.
/// </summary>
/// <param name="value"></param>
public static CssBuilder Default(string value) => new CssBuilder(value);
/// <summary>
/// Creates an Empty CssBuilder used to define conditional CSS classes used in a component.
/// Call Build() to return the completed CSS Classes as a string.
/// </summary>
public static CssBuilder Empty() => new CssBuilder();
/// <summary>
/// Creates a CssBuilder used to define conditional CSS classes used in a component.
/// Call Build() to return the completed CSS Classes as a string.
/// </summary>
/// <param name="value"></param>
public CssBuilder(string value) => stringBuffer = value;
/// <summary>
/// Adds a raw string to the builder that will be concatenated with the next class or value added to the builder.
/// </summary>
/// <param name="value"></param>
/// <returns>CssBuilder</returns>
public CssBuilder AddValue(string value)
{
stringBuffer += value;
return this;
}
/// <summary>
/// Adds a CSS Class to the builder with space separator.
/// </summary>
/// <param name="value">CSS Class to add</param>
/// <returns>CssBuilder</returns>
public CssBuilder AddClass(string value) => AddValue(" " + value);
/// <summary>
/// Adds a conditional CSS Class to the builder with space separator.
/// </summary>
/// <param name="value">CSS Class to conditionally add.</param>
/// <param name="when">Condition in which the CSS Class is added.</param>
/// <returns>CssBuilder</returns>
public CssBuilder AddClass(string value, bool when = true) => when ? this.AddClass(value) : this;
/// <summary>
/// Adds a conditional CSS Class to the builder with space separator.
/// </summary>
/// <param name="value">CSS Class to conditionally add.</param>
/// <param name="when">Condition in which the CSS Class is added.</param>
/// <returns>CssBuilder</returns>
public CssBuilder AddClass(string value, Func<bool> when = null) => this.AddClass(value, when());
/// <summary>
/// Adds a conditional CSS Class to the builder with space separator.
/// </summary>
/// <param name="value">Function that returns a CSS Class to conditionally add.</param>
/// <param name="when">Condition in which the CSS Class is added.</param>
/// <returns>CssBuilder</returns>
public CssBuilder AddClass(Func<string> value, bool when = true) => when ? this.AddClass(value()) : this;
/// <summary>
/// Adds a conditional CSS Class to the builder with space separator.
/// </summary>
/// <param name="value">Function that returns a CSS Class to conditionally add.</param>
/// <param name="when">Condition in which the CSS Class is added.</param>
/// <returns>CssBuilder</returns>
public CssBuilder AddClass(Func<string> value, Func<bool> when = null) => this.AddClass(value, when());
/// <summary>
/// Adds a conditional nested CssBuilder to the builder with space separator.
/// </summary>
/// <param name="value">CSS Class to conditionally add.</param>
/// <param name="when">Condition in which the CSS Class is added.</param>
/// <returns>CssBuilder</returns>
public CssBuilder AddClass(CssBuilder builder, bool when = true) => when ? this.AddClass(builder.Build()) : this;
/// <summary>
/// Adds a conditional CSS Class to the builder with space separator.
/// </summary>
/// <param name="value">CSS Class to conditionally add.</param>
/// <param name="when">Condition in which the CSS Class is added.</param>
/// <returns>CssBuilder</returns>
public CssBuilder AddClass(CssBuilder builder, Func<bool> when = null) => this.AddClass(builder, when());
/// <summary>
/// Adds a conditional CSS Class when it exists in a dictionary to the builder with space separator.
/// Null safe operation.
/// </summary>
/// <param name="additionalAttributes">Additional Attribute splat parameters</param>
/// <returns>CssBuilder</returns>
public CssBuilder AddClassFromAttributes(IReadOnlyDictionary<string, object> additionalAttributes) =>
additionalAttributes == null ? this :
additionalAttributes.TryGetValue("class", out var c) ? AddClass(c.ToString()) : this;
/// <summary>
/// Finalize the completed CSS Classes as a string.
/// </summary>
/// <returns>string</returns>
public string Build()
{
// String buffer finalization code
return stringBuffer != null ? stringBuffer.Trim() : string.Empty;
}
// ToString should only and always call Build to finalize the rendered string.
public override string ToString() => Build();
}
public static class BuilderExtensions
{
/// <summary>
/// Used to convert a CssBuilder into a null when it is empty.
/// Usage: class=null causes the attribute to be excluded when rendered.
/// </summary>
/// <param name="builder"></param>
/// <returns>string</returns>
public static string NullIfEmpty(this CssBuilder builder) =>
string.IsNullOrEmpty(builder.ToString()) ? null : builder.ToString();
/// <summary>
/// Used to convert a StyleBuilder into a null when it is empty.
/// Usage: style=null causes the attribute to be excluded when rendered.
/// </summary>
/// <param name="builder"></param>
/// <returns>string</returns>
public static string NullIfEmpty(this StyleBuilder builder) =>
string.IsNullOrEmpty(builder.ToString()) ? null : builder.ToString();
/// <summary>
/// Used to convert a string.IsNullOrEmpty into a null when it is empty.
/// Usage: attribute=null causes the attribute to be excluded when rendered.
/// </summary>
/// <param name="builder"></param>
/// <returns>string</returns>
public static string NullIfEmpty(this string s) =>
string.IsNullOrEmpty(s) ? null : s;
}
}
SpinnerType
Components/SpinnerType.cs
namespace Spinner.Server.Components
{
public enum SpinnerType
{
Circle,
CircleFade
}
}
SpinnerBase
Components/SpinnerBase.cs
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Spinner.Server.Components
{
public class SpinnerBase : ComponentBase
{
[Parameter] public string Color { get; set; }
[Parameter] public bool Center { get; set; }
[Parameter] public string Size { get; set; }
[Parameter(CaptureUnmatchedValues = true)]
public IReadOnlyDictionary<string, object> AdditionalAttributes { get; set; } = new Dictionary<string, Object>();
}
}
SpinnerContainer
Components/SpinnerContainer.razor
@namespace Spinner.Server.Components
@inherits SpinnerBase
<div @attributes="AdditionalAttributes" class="@CssClass" style="@Style">
@ChildContent
</div>
@code {
[Parameter] public RenderFragment ChildContent { get; set; }
[Parameter] public string BaseCss { get; set; }
string CssClass =>
CssBuilder.Default(BaseCss)
.AddClass("sk-center", Center)
.AddClassFromAttributes(AdditionalAttributes)
.Build();
string Style =>
StyleBuilder.Empty()
.AddStyle("--sk-color", Color, when: !string.IsNullOrEmpty(Color))
.AddStyle("--sk-size", Size, when: !string.IsNullOrEmpty(Size))
.AddStyleFromAttributes(AdditionalAttributes)
.NullIfEmpty();
}
Spinner type implementations
- More types: https://github.com/EdCharbeneau/BlazorPro.Spinkit/tree/master/src/BlazorPro.Spinkit/Spinners
Components/Circle.razor
@namespace Spinner.Server.Components
@inherits SpinnerBase
<SpinnerContainer Center="@Center" Color="@Color" Size="@Size" @attributes="AdditionalAttributes" BaseCss="sk-circle">
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
<div class="sk-circle-dot"></div>
</SpinnerContainer>
Components/CircleFade.razor
@namespace Spinner.Server.Components
@inherits SpinnerBase
<SpinnerContainer Center="@Center" Color="@Color" Size="@Size" @attributes="AdditionalAttributes" BaseCss="sk-chase">
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
<div class="sk-circle-fade-dot"></div>
</SpinnerContainer>
DefaultSpinnerChooser
Components/DefaultSpinnerChooser.razor
@namespace Spinner.Server.Components
@inherits SpinnerBase
@switch (DefaultSpinner)
{
case SpinnerType.Circle:
<Circle Center="@Center" Size="@Size" Color="@Color" @attributes="AdditionalAttributes" />
break;
case SpinnerType.CircleFade:
<CircleFade Center="@Center" Size="@Size" Color="@Color" @attributes="AdditionalAttributes" />
break;
default:
<CircleFade Center="@Center" Size="@Size" Color="@Color" @attributes="AdditionalAttributes" />
break;
}
@code {
[Parameter] public SpinnerType DefaultSpinner { get; set; }
}
SpinLoader
Components/SpinLoader.razor
@inherits SpinnerBase
@if (IsLoading)
{
if (LoadingTemplate == null)
{
<div class="row" style="min-height: 50rem;">
<div class="col-sm-12 my-sm-auto">
<CircleFade Center="true" />
</div>
</div>
}
else
{
@LoadingTemplate
}
}
else
{
if (!IsFaulted || FaultedContentTemplate == null)
{
@ContentTemplate
}
if (IsFaulted)
{
@FaultedContentTemplate
}
}
@code {
[Parameter] public SpinnerType Spinner { get; set; }
[Parameter] public bool IsLoading { get; set; }
[Parameter] public bool IsFaulted { get; set; }
[Parameter] public RenderFragment LoadingTemplate { get; set; }
[Parameter] public RenderFragment ContentTemplate { get; set; }
[Parameter] public RenderFragment FaultedContentTemplate { get; set; }
}
Usage
- Create a Blazor server (demo) app
- Use spinner in
Pages/FetchData.razor
- Demo/sample app : https://github.com/hovermind/blazor-demos/tree/master/Spinner.Server
Pages/FetchData.razor
@page "/fetchdata"
@using Spinner.Server.Data
@inject WeatherForecastService ForecastService
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from a service.</p>
<SpinLoader IsLoading="@(forecasts == null)">
<ContentTemplate>
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
</ContentTemplate>
</SpinLoader>
@code {
private WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
await Task.Delay(2000);
forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
}
}
Better Way To Use Custom Spinner
Components/BaseComponent.cs
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.JSInterop;
using Serilog;
using System;
using System.Linq;
using System.Threading.Tasks;
public class BaseComponent: ComponentBase, IDisposable
{
[Inject]
protected NavigationManager NavigationManager { get; set; }
[Inject]
protected IJSRuntime JSRuntime { get; set; }
protected string ErroMessage { get; set; } = string.Empty;
// ... ... ...
protected bool ShowSpinner { get; set; } = true;
public void Dispose()
{
ShowSpinner = false;
}
// ... ... ...
}
Pages/Index.razor.cs
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
using Serilog;
using Microsoft.AspNetCore.WebUtilities;
using System.Linq;
namespace Server.Spinner.Pages
{
public class IndexBase : BaseComponent
{
protected override async Task OnAfterRenderAsync(bool firstRender)
{
//
// Lifecycle callback method will be called twice => use firstRender flag
//
if (firstRender)
{
ErroMessage = string.Empty;
try
{
// ... ... ...
// DB operation here (async await)
}
catch (AppException ex) // logging will be done in service
{
ErroMessage = ex.Message;
}
catch (Exception ex) // do logging
{
Log.Error(ex.Message, ex);
ErroMessage = "Unexpected error occured!";
}
ShowSpinner = !ShowSpinner;
StateHasChanged();
}
}
}
}
Pages/Index.razor
@page "/"
@page "/items"
@inherits IndexBase
<SpinLoader IsLoading="@ShowSpinner">
<ContentTemplate>
<div class="row ">
<!-- ... ... ... -->
</div>
<div class="row">
<!-- ... ... ... -->
</div>
</ContentTemplate>
</SpinLoader>