| license | MIT |
|---|---|
| name | spinner-overlay |
| description | Render the Syncfusion Blazor Toolkit SfSpinner as a portal-style overlay (full-page, full-region, modal style, or focus-trapped modal). Covers z-index stacking, focus trap, aria-modal semantics, body scroll lock, and the difference between render in Static SSR vs interactive runtimes. |
SfSpinner is often used as a modal-style overlay during long-running work.
Four overlay shapes are common; each has a known Composition recipe.
Use when the entire page is being replaced or processed (e.g. "Submitting order, please wait…").
@implements IDisposable
@if (_isProcessing)
{
<div class="page-overlay" role="alert" aria-busy="true">
<SfSpinner @bind-Visible="@_isProcessing"
ZIndex="9000"
Size="56"
Thickness="6"
Label="Submitting your order…" />
</div>
}
@code {
private bool _isProcessing;
private async Task OnSubmit()
{
_isProcessing = true;
await InvokeAsync(StateHasChanged); // render the overlay NOW, before work
try
{
await OrderService.SubmitAsync(order);
}
finally
{
_isProcessing = false;
}
}
}.page-overlay {
position: fixed;
inset: 0;
background: rgba(255,255,255,0.72);
z-index: 9000;
display: flex;
align-items: center;
justify-content: center;
}InvokeAsync(StateHasChanged) before the work runs is critical — otherwise
the overlay renders after the await, defeating its purpose.
role="alert" + aria-busy="true" keeps screen readers informed; combined
with Label on the spinner, this is the WCAG 2.1 minimum.
Use when one part of the page is loading (e.g. a chart area). The overlay
positioning is relative to the parent.
<div class="dashboard-region">
<SfChart @ref="chartRef" />
@if (isRefreshing)
{
<div class="region-overlay" aria-busy="true">
<SfSpinner @bind-Visible="@isRefreshing"
Size="36"
Thickness="4"
Label="Refreshing data" />
</div>
}
</div>
<style>
.dashboard-region { position: relative; min-height: 320px; }
.region-overlay {
position: absolute;
inset: 0;
background: rgba(255,255,255,0.6);
display: flex; align-items: center; justify-content: center;
backdrop-filter: blur(2px);
}
</style>Rule: The region container must have explicit min-height — otherwise the
overlay collapses around the spinner and looks broken.
Show spinner while a form submits. Pattern: render the form with the submit button disabled; render the spinner in a sibling overlay that covers the form during submission.
@using Syncfusion.Blazor.Toolkit
<div class="form-container">
<EditForm Model="formData" OnSubmit="@HandleSubmit">
<InputText @bind-Value="formData.Name" placeholder="Enter name" />
<button type="submit" disabled="@isSubmitting">Submit</button>
</EditForm>
@if (isSubmitting)
{
<div class="form-overlay" role="alert" aria-busy="true">
<SfSpinner @bind-Visible="@isSubmitting" Label="Submitting form..." />
</div>
}
</div>
@code {
private FormModel formData = new();
private bool isSubmitting = false;
private async Task HandleSubmit()
{
isSubmitting = true;
try
{
await Task.Delay(2000); // Simulate API call
}
finally
{
isSubmitting = false;
}
}
private class FormModel
{
public string Name { get; set; }
}
}Two decisions to be aware of:
- Spinner inside the form? No. Keep the spinner adjacent (in the overlay) — nesting it inside the submit button breaks the click target.
- Use
aria-busy="true"on the overlay container — the spinner itself isn't a live-region node; the wrapper carries the screen-reader announcement.
Combines SfDialog + SfSpinner for a "we're working, do not interact"
modal:
<SfDialog @bind-Visible="_showWorking"
IsModal="true"
ShowCloseIcon="false"
AllowPrerender="false"
Width="320px">
<DialogTemplates>
<Content>
<div style="text-align:center;">
<SfSpinner Visible="true"
Size="48"
Thickness="6"
Label="Working…" />
</div>
</Content>
</DialogTemplates>
</SfDialog>Notes:
ShowCloseIcon="false"keeps the user from dismissing the modalAllowPrerender="false"prevents the spinner from rendering twice on dialog open in interactive render modesIsModal="true"traps focus inside the dialog
Long-running work where the user must NOT scroll the underlying page (checkout, autosave, etc.). Pair the overlay with manual scroll lock:
@implements IDisposable
@if (_isProcessing)
{
<div class="page-overlay" …> … </div>
}
@code {
private bool _isProcessing;
protected override void OnAfterRender(bool firstRender)
{
if (_isProcessing) BodyLock();
else BodyUnlock();
}
private void BodyLock() => Document.Body.Style.Overflow = "hidden";
private void BodyUnlock() => Document.Body.Style.Overflow = "";
[Inject] private IJSRuntime JS { get; set; } = default!;
// alternative: use IJSRuntime to set document.body.style.overflow
public void Dispose() => BodyUnlock();
}OnAfterRender is the only safe place to touch the DOM. Dispose releases
the lock on navigation away.
Components live in stacked layers; pick a slot and stick to it:
| Layer | Z-index | Use |
|---|---|---|
| bage content | 0 | Default |
| Sticky header / footer | 100/200 | Navigation chrome |
| Floating action button | 500 | Primary screens |
| Tooltip / popover | 1500 | SfTooltip, SfDropdown |
| Spinner overlay | 9000 | Recommended |
Modal dialog (SfDialog) |
9500 | Override the overlay if both are shown |
| Toast / Snackbar | 9800 | Async notifications |
Use a constants file (@layers { inline or app.css) to document the layer
stack — agents will reference it before assuming a number.
A focus trap is mandatory only when the user cannot interact with the
underlying page. Use the Syncfusion SfDialog with IsModal="true" — it
handles focus trap automatically.
For non-interactive overlays (the rest of the page is still visible but the
actions should be paused), use plain CSS and a manual flag — and set
tabindex="-1" on overlay so it doesn't take focus.
In Static SSR (-int None) render mode, OnAfterRender does not run on
the server. The overlay renders only on the client. Also: IJSRuntime is fine
in Static SSR but document.body access requires JS interop.
For Static SSR overlays, prefer element-only overlay (no body scroll lock):
<div class="page-overlay-static">
<SfSpinner @ref="spinner"
@bind-Visible="@isBusy"
Label="Loading…" />
</div>…where page-overlay-static is a small region (e.g. inside a <section>),
not a position: fixed overlay that locks the body.
Overlay re-renders every @bind-Visible change. If the spinner content
changes during the overlay (e.g. label updates), do:
@if (_isProcessing)
{
<SfSpinner @bind-Visible="@_isProcessing"
@key="@_currentLabel"
Label="@_currentLabel" />
}
@code {
private string _currentLabel = "Loading…";
private async Task Tick()
{
for (int i = 0; i <= 100; i += 25)
{
_currentLabel = $"Loading… {i}%";
await Task.Delay(500);
await InvokeAsync(StateHasChanged);
}
}
}@key="@_currentLabel" forces a full remount on label change, smoothing the
text transition.
- Don't double-bind —
Visibleand@bind-Visibletogether causes re-render storms - Don't trap focus manually when
IsModal="true"already does it - Don't set
ZIndex="9999999"— you'll fight toast/modal slots - Don't use
position: fixedoverlays in Static SSR without first testing that the page-level layout doesn't break - Don't unlock body scroll on modal close without also clearing the overlay (causes "stuck scroll")