Drop-in spinner for any Angular 17+ button (no CSS imports): ngxsmk-button-spinner

Drop-in spinner for any Angular 17+ button (no CSS imports): ngxsmk-button-spinner

Add a loading spinner to any existing <button> with one attribute.
No component swap. No global stylesheet. SSR-safe. A11y-friendly.

✨ What it is

ngxsmk-button-spinner is a tiny Angular 17+ directive that overlays a spinner on your button during async work.

  • Drop-in: [ngxsmkButtonSpinner]="loading"
  • Two modes

    • Inline (default): spinner appears after the text with a small gap
    • Overlay (centered): [ngxsmkButtonSpinnerHideLabel]="true"
  • Zero CSS imports — styles are injected once

  • CSS variables theming (bind directly in the template)

  • A11y: role="status", configurable aria-label

  • SSR-safe

🧩 Install

npm i ngxsmk-button-spinner

Peer deps: @angular/core@>=17, @angular/common@>=17.

🚀 Quick start (Signals)

// app.component.ts
import { Component, signal } from '@angular/core';
import { NgxSmkButtonSpinnerDirective } from 'ngxsmk-button-spinner';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [NgxSmkButtonSpinnerDirective],
  template: `
    <button [ngxsmkButtonSpinner]="saving()" (click)="save()">
      Save
    </button>
  `
})
export class AppComponent {
  saving = signal(false);

  save() {
    this.saving.set(true);
    setTimeout(() => this.saving.set(false), 1200);
  }
}

That’s it. No module boilerplate. No style imports.

🎛️ Theming (recommended): bind CSS variables directly

Bind these CSS variables right on the button for instant updates (no restarts, no object cloning):

  • --ngxsmk-color (spinner color)
  • --ngxsmk-track (trail color, default transparent)
  • --ngxsmk-thickness (2px, 3px, …)
  • --ngxsmk-size (20px, 1.2em, …)
  • --ngxsmk-speed (700ms, 450ms, …)

Inline (default)

<button
  [ngxsmkButtonSpinner]="loading"
  [style.--ngxsmk-color]="'#0ea5e9'"
  [style.--ngxsmk-thickness]="'2px'"
  [style.--ngxsmk-size]="'22px'"
  [style.--ngxsmk-speed]="'600ms'">
  Publish
</button>

Overlay (centered, text hidden)

<button
  [ngxsmkButtonSpinner]="loading"
  [ngxsmkButtonSpinnerHideLabel]="true"
  [style.--ngxsmk-color]="'#0ea5e9'"
  [style.--ngxsmk-thickness]="'6px'"
  [style.--ngxsmk-size]="'20px'"
  [style.--ngxsmk-speed]="'500ms'">
  Save
</button>

Prefer variable binding over passing a theme object — it updates immediately with Angular HMR/Signals and avoids change-detection gotchas.

You can also set per-class defaults:

/* component.css */
.primary-btn {
  --ngxsmk-color: #2563eb;
  --ngxsmk-thickness: 3px;
  --ngxsmk-size: 22px;
  --ngxsmk-speed: 450ms;
}

🧠 API (tiny)

<button
  [ngxsmkButtonSpinner]="loading"         <!-- boolean or Signal<boolean> -->
  [ngxsmkButtonSpinnerHideLabel]="true"   <!-- optional overlay mode -->
  [ngxsmkButtonSpinnerOptions]="{ ariaLabel: 'Saving' }">
  Save
</button>
  • ngxsmkButtonSpinner: boolean | Signal<boolean) — toggles spinner; also disables the button.
  • ngxsmkButtonSpinnerHideLabel?: boolean | Signal<boolean) — overlay mode when true.
  • ngxsmkButtonSpinnerOptions?: { ariaLabel?: string } — a11y label for the spinner.

♿ Accessibility

  • Spinner uses role="status" with an aria-label (default: “Loading”).
  • Button is disabled while loading to prevent duplicate submissions.
  • Overlay mode hides the label visually but keeps the status for screen readers.

🌐 SSR / hydration

The directive injects DOM only in the browser after view init. Toggle loading in response to user actions or browser-only effects.

🛠️ Troubleshooting

  • Theme not updating
    Bind CSS variables directly ([style.--ngxsmk-*]). This updates instantly without rebuilding.
  • Spinner not centered in overlay
    Ensure the button doesn’t clip positioned children with overflow: hidden.
  • Text not hiding in overlay
    Upgrade to the latest build — the label is wrapped and hidden safely.

📦 Links

If you build something with it, drop a comment — I’d love to see it!

Similar Posts