A tiny Angular 17+ directive that adds a loading spinner to any existing <button> — no component swap and no global stylesheet required.
-
✅ Drop-in attribute:
[ngxsmkButtonSpinner]="loading" -
✅ Two display modes:
- Inline (default): spinner appears after the text with a small gap
- Overlay (centered): set
[ngxsmkButtonSpinnerHideLabel]="true"to hide text and center the spinner
-
✅ Theming via CSS variables (bind directly in the template)
-
✅ A11y-friendly (
role="status", configurable aria-label) -
✅ Strict TypeScript & SSR-safe
npm i ngxsmk-button-spinnerPeer deps: @angular/core@>=17, @angular/common@>=17.
// Any standalone component
import { Component, signal } from '@angular/core';
import { NgxSmkButtonSpinnerDirective } from 'ngxsmk-button-spinner';
@Component({
selector: 'demo',
standalone: true,
imports: [NgxSmkButtonSpinnerDirective],
template: `
<button [ngxsmkButtonSpinner]="saving()" (click)="save()">
Save
</button>
`
})
export class DemoComponent {
saving = signal(false);
save() {
this.saving.set(true);
setTimeout(() => this.saving.set(false), 1500);
}
}That’s it — no stylesheet import needed.
<button
[ngxsmkButtonSpinner]="loading"
[ngxsmkButtonSpinnerHideLabel]="true" <!-- optional: overlay mode -->
[ngxsmkButtonSpinnerOptions]="{ ariaLabel: 'Saving' }"
>
Save
</button>| Input | Type | Default | Description |
|---|---|---|---|
ngxsmkButtonSpinner |
boolean |
false |
When true, disables the button and shows the spinner. |
ngxsmkButtonSpinnerHideLabel |
boolean |
false |
If true, hides text and centers spinner over the button (overlay). |
ngxsmkButtonSpinnerOptions |
{ ariaLabel?: string } |
{ ariaLabel: 'Loading' } |
A11y label for the spinner. |
Customize per button by binding these CSS variables. They update reactively — no restarts, no object cloning.
Supported variables (bind any subset):
--ngxsmk-color(e.g.#0ea5e9orcurrentColor)--ngxsmk-track(spinner trail color, defaulttransparent)--ngxsmk-thickness(e.g.2px)--ngxsmk-size(e.g.20px,1.2em)--ngxsmk-speed(e.g.700ms)
<button
[ngxsmkButtonSpinner]="loading"
[style.--ngxsmk-color]="'#0ea5e9'"
[style.--ngxsmk-thickness]="'1px'"
[style.--ngxsmk-size]="'60px'"
[style.--ngxsmk-speed]="'1500ms'"
>
Save
</button>Overlay example (centered, text hidden)
<button
[ngxsmkButtonSpinner]="loading"
[ngxsmkButtonSpinnerHideLabel]="true"
[style.--ngxsmk-color]="'#0ea5e9'"
[style.--ngxsmk-thickness]="'8px'"
[style.--ngxsmk-size]="'20px'"
[style.--ngxsmk-speed]="'500ms'"
>
Save
</button>Tip: You can also set these variables in component CSS:
.primary-btn { --ngxsmk-color: #2563eb; --ngxsmk-thickness: 3px; --ngxsmk-size: 22px; --ngxsmk-speed: 450ms; }
<button [ngxsmkButtonSpinner]="loading">Submit</button><button [ngxsmkButtonSpinner]="loading" [ngxsmkButtonSpinnerHideLabel]="true">
Save
</button><button
[ngxsmkButtonSpinner]="loading"
class="primary-btn"
[style.--ngxsmk-color]="'#2563eb'"
[style.--ngxsmk-thickness]="'3px'"
[style.--ngxsmk-size]="'22px'"
>
Publish
</button><button
[ngxsmkButtonSpinner]="loading"
[style.--ngxsmk-speed]="'450ms'"
[style.--ngxsmk-size]="'1.4em'"
>
Sync
</button>- Spinner element uses
role="status"andaria-label(default"Loading"). - The directive sets
disabledon the button while loading. - If you hide the label (overlay mode), screen readers still get the status via the spinner’s aria label.
This directive avoids DOM APIs on the server. Spinner elements and the small <style> tag are injected only in the browser after view init. For best UX, toggle loading in response to user actions or browser-only effects.
-
ReferenceError: document is not defined Ensure you’re using the latest directive: it guards DOM access with
PLATFORM_IDand only injects styles in the browser. -
TS2532: Object is possibly 'undefined' The directive is strict-mode safe. If you modified it, null‑check optional fields before use.
-
TS5076:
&&and??cannot be mixed Parenthesize or simplify. Example:const keep = theme?.keepLabel ?? false; -
Spinner not visible In overlay mode the label is hidden via a wrapper (
visibility: hidden) while the spinner is centered in an overlay box. Ensure your button isn’t applyingoverflow: hiddenin a way that clips the overlay.
// public-api.ts
export * from './lib/button-spinner.directive';
export * from './lib/types';export interface NgxSmkButtonSpinnerOptions {
ariaLabel?: string;
}
export interface NgxSmkButtonSpinnerTheme {
color?: string;
trackColor?: string;
thickness?: string;
size?: string;
speedMs?: number;
}- Supports Angular 17+ (standalone).
- Semantic versioning. Breaking changes (if any) will bump the major.
MIT
