| license | MIT | ||||
|---|---|---|---|---|---|
| name | syncfusion-blazor-toolkit-notifications | ||||
| description | Implement Syncfusion Blazor Toolkit loading and activity indicators — SfSpinner. USE FOR: form submission and async-operation feedback, content loading decorations, overlay compositions with z-index stacking, accessibility- compliant loading announcements (aria-live), programmatic visibility toggles via @bind-Visible or VisibleChanged, and cancelable open/close events. REQUIRES interactive render mode (Server, WebAssembly, or Auto @ .NET 8+). Overlay patterns require JS interop for body scroll lock. DO NOT USE FOR: full-page skeleton loaders (use SfSkeleton — not in this skill), action-bearing toast notifications (use SfToast — not in this skill), or progress bars with explicit percent (use SfProgressBar — not in this skill). | ||||
| compatibility | .NET 8+, render-modes: Server, WebAssembly, Auto | ||||
| metadata |
|
⚙️ Render mode:
SfSpinnerrequires an interactive render mode (Server, WebAssembly, or Auto @ .NET 8+). In Static SSR, prefer the built-in[StreamRendering]attribute on the page (noSfSpinnerneeded) — seereferences/spinner-overlay.mdfor the static-mode escape hatch. ReadAGENTS.mdbefore picking a strategy.
The Notifications components provide visual feedback during asynchronous operations and content loading scenarios. This skill guides you through implementing the Spinner component for indicating background processing.
- Use
@bind-Visiblefor state OR useVisible+VisibleChanged— never both. Combine = re-render storms and "stuck open" spinners. OnOpenandOnCloseargs.Cancelaborts the action; they run before the visible change fires. Don't expect the spinner to hide synchronously afterOnClosereturns.Labelis announced via aria-live automatically. Either:- Set
Label="Loading…"and don't put text inside<Template>, OR - Use
<Template>and addaria-hidden="true"on the visual nodes. Never both.
- Set
- Spinner renders inline. For overlay positioning, supply
CssClasswithposition: absolute/fixedrules. - body scroll lock requires JS interop (
IJSRuntime). It's not free — wireIAsyncDisposable.DisposeAsync()to release the lock. VisibleChangedisEventCallback<bool>— fire-and-forget handlers will swallow exceptions. Useasync Taskhandlers.- Render the overlay via
InvokeAsync(StateHasChanged)BEFORE the long-runningawait, otherwise the overlay renders after the await and serves no purpose. - Don't inset
SfSpinnerinside an interactive button — that defeats the click target. Use a custom button replacement oraria-hidden.
| Anti-pattern | Symptom | Fix |
|---|---|---|
Visible="@_busy" @bind-Visible VisibleChanged="@(v => _busy = v)" |
Re-render storm; spinner "stuck open" or "stuck closed" | Pick exactly one: @bind-Visible="@_busy" |
Using SfSpinner for confirmation ("are you sure?") |
User double-clicks the button before the spinner fades out; double-commit | Use Syncfusion.Blazor.Toolkit.Popups.SfDialog with IsModal="true" |
position: fixed overlay inside a Static SSR page |
JS not available pre-interactivity; overlay renders without scroll lock |
Either upgrade to interactive render mode, or rely on element-level overlay (don't lock body scroll) |
<SfSpinner> nested inside <SfButton> |
Pointer events pass through; user clicks the button while spinner is "showing" | Use Disabled="@_busy" on the button, and place <SfSpinner> adjacent to (not inside) the button |
No role="alert" / aria-busy="true" on the parent overlay element |
Screen readers don't know the page is busy; user confused | Wrap with <div role="alert" aria-busy="true"> while the spinner is up |
Label="" (or omitted) |
WCAG 2.1 violation; spinner announces nothing visually either | Always set Label="Loading…" with at least 3 chars / aria-friendly text |
| Disabling the parent button to "pause interaction" | Confusing — button looks inert but no feedback | Use a visible spinner overlay with explicit pointer-events: none, or render the button as "Saving…" text while disabled |
OnClose setting args.Cancel = true and then mutating Visible directly |
OnClose runs sync; race condition opens the spinner |
args.Cancel = true only; let the binding decide visibility |
Two SfSpinner instances bound to the same bool |
Two overlay layers stacked; performance + z-index fights | One spinner; resize via CssClass |
OnOpen = async void |
Exceptions silently escape | OnOpen = async Task … or use ValueTask |
Bad:
<SfSpinner @ref="spinner"
Visible="@_busy"
@bind-Visible="@_busy"
VisibleChanged="@(v => _busy = v)"
Label="Saving…" />
<!-- infinite re-renders -->Fix: pick exactly one. @bind-Visible is enough:
<SfSpinner @bind-Visible="@_busy" Label="Saving…" />Bad:
private async Task Submit()
{
_busy = true;
await SaveAsync(); // overlay exists in markup but block is synchronous on first render
_busy = false;
}Why it fails: even though _busy = true triggers a render, the
async method starts the SaveAsync call immediately and re-renders again
only when awaited. Browser paints on the second invocation, after completion.
Fix: force a render first, then await:
private async Task Submit()
{
_busy = true;
await InvokeAsync(StateHasChanged); // paint overlay NOW
try { await SaveAsync(); }
finally { _busy = false; }
}Anti-pattern: raise the spinner for 3s before commit, hoping the user will click away.
Fix: use a modal dialog (load syncfusion-blazor-toolkit-dialog).
<SfDialog @bind-Visible="_confirm" IsModal="true" Header="Confirm Save">
<DialogButtons>
<DialogButton Content="Yes" OnClick="@ConfirmYes" />
<DialogButton Content="No" />
</DialogButtons>
</SfDialog>
@code {
private async Task ConfirmYes() { _confirm = false; _busy = true; await InvokeAsync(StateHasChanged); await SaveAsync(); _busy = false; }
}Spinner for post-confirmation (during actual save); dialog gates user input.
Workflow 4 — Agent's overlay uses body { overflow: hidden } but never clears
Bad:
@if (_busy) <SfSpinner @bind-Visible="@_busy" />
@code { protected override void OnAfterRender(bool _) { if (_busy) JS.InvokeVoidAsync("lockBody"); } }
<!-- user navigates away; lock persists -->Fix: centralize lock/unlock in IAsyncDisposable:
@implements IAsyncDisposable
@code {
protected override void OnAfterRender(bool _) { if (_busy) JS.InvokeVoidAsync("lockBody"); }
public async ValueTask DisposeAsync() { try { await JS.InvokeVoidAsync("unlockBody"); } catch { } }
}Anti-pattern:
<SfButton Disabled="@_busy" OnClick="SubmitAsync">
@if (_busy) <SfSpinner Visible="true" Size="14" />
Submit
</SfButton>Why it fails: in Static SSR, the spinner DOM never mounts; pointer events on the button area conflict with the inner spinner's wrapper.
Fix #1 — label swap (simplest):
<SfButton Disabled="@_busy" OnClick="SubmitAsync">
@(_busy ? "Saving…" : "Submit")
</SfButton>Fix #2 — external spinner overlay around the whole form: see references/spinner-overlay.md §2.5 for the canonical EditForm + form-overlay composition pattern.
See also: references/spinner-template.md (custom visuals + composition) and references/spinner-overlay.md (full-page, region, modal overlays).
@using Syncfusion.Blazor.Toolkit
@if (isLoading)
{
<SfSpinner @bind-Visible="@isLoading" Label="Loading data..." />
}
@code {
private bool isLoading = true;
}For installation, services, visibility control, labels, CSS classes, and size/positioning see references/spinner-implementation.md.
| Need | Read |
|---|---|
| Setup, services, theme, visibility, labels, CSS, sizing | references/spinner-implementation.md |
OnOpen / OnClose / Created / Destroyed, cancellation, templates |
references/spinner-events-customization.md |
| WCAG / keyboard / screen reader | references/accessibility-best-practices.md |
Branded visuals, SpinType, CSS variables |
references/spinner-template.md |
| Overlay patterns (page / region / modal / scroll-lock) | references/spinner-overlay.md |
- Setup: references/spinner-implementation.md
- Overlay patterns: references/spinner-overlay.md