Angular Loading Spinner & Overlay | ng-hub-ui-loading
Angular loading component with spinner, dots, bars, pulse and ring indicators in inline, overlay and fullscreen modes, plus a service for app-wide blocking overlays.
Last updated Sep 28, 2026
Overview
Why teams search for this library
Use ng-hub-ui-loading when a section, a container or the whole application has to signal work in progress, without every view hand-rolling its own spinner and scrim.
Install
npm install ng-hub-ui-loadingJump to
Ideal for
- route and data loading
- form submission overlays
- app-wide blocking operations
- branded splash screens
- page progress bars under the navbar
About loading
ng-hub-ui-loading is a standalone Angular component that renders an indeterminate loading state in three modes: `inline` as a block in the document flow, `overlay` absolutely positioned over its parent container, and `fullscreen` fixed to the viewport. It ships five pure-CSS indicators — spinner, dots, bars, pulse and ring — in three sizes, an optional message and an optional brand image animated with `none`, `spin` or `pulse`. `HubLoadingService` drives the global fullscreen overlay programmatically and reference-counts concurrent callers, so parallel requests never tear the overlay away from each other. Every colour, size and duration is a `--hub-loading-*` custom property, with a `hub-loading-theme()` Sass mixin for wholesale restyling, and the component is accessible out of the box through `role=status`, `aria-live=polite` and `aria-busy`, honouring `prefers-reduced-motion`. Alongside it, `hub-loading-bar` reports that the page itself is on its way — the thin strip under the navbar, rendered in the document flow, attached to a positioned navbar or fixed to the viewport at `--hub-loading-bar-offset`. `HubLoadingBarService` reference-counts its callers in the same way, waits out a grace period so work that finishes quickly never flashes a bar, and trickles towards an end it never reaches on its own: only `complete()` may show 100%. `provideHubLoadingBarRouter()` and `hubLoadingBarInterceptor` wire it to navigation and to HTTP without either knowing about the other.
Feature guides
Inline Block
`inline` is the default mode because a region that is still empty should already reserve the space its content will take: the block joins normal document flow, so nothing jumps when the real content arrives. `message` renders under the indicator, and anything projected between the tags renders under the message — the place for a cancel action or a note that the operation is taking longer than usual. The host carries `role="status"`, `aria-live="polite"` and `aria-busy="true"`, so a screen reader picks the message up as it appears without interrupting whatever it is already reading.
Examples:
Inline Block Basics
The inline block in its three useful forms: indicator only, indicator with a message, and a message with a cancel action projected underneath.
The block is announced as role="status" with aria-live="polite", so a screen reader reads the message when it appears without interrupting whatever is being read.
Code
Import:
Template:
Component:
Indicators And Accents
Five pure-CSS indicators ship with the library — `spinner`, `dots`, `bars`, `pulse` and `ring` — so it pulls in no image asset and no icon font. Three size steps preset `--hub-loading-size`, and the token stays overridable on its own for a layout the steps do not cover. `color` is not an ordinary CSS colour: it goes through `resolveHubAccent()`, which reads a bare name such as `primary` as a design-system token that follows the theme, while a hex, an `oklch()` literal or a `var(...)` reference is used verbatim.
Examples:
Variants, Sizes And Accents
The five bundled indicators, the three size steps, and the three shapes an accent can take — a semantic name, a CSS colour literal and a `var(...)` reference.
Variants
spinnerdotsbarspulseringSizes
size="sm"size="md"size="lg" Each step is a preset for the --hub-loading-size token. Override that token directly when a layout needs a size the three steps do not cover.
Accents
color="primary"color="success"color="danger"color="#7c3aed"color="var(--hub-sys-color-warning)" A bare name resolves to var(--hub-sys-color-<name>) and tracks the active theme. A hex, an oklch() or a var(...) is used verbatim.
Code
Import:
Template:
Component:
Overlay And Fullscreen
`overlay` covers one region instead of the page. It is positioned absolutely, so the nearest positioned ancestor decides what gets covered — give that container `position: relative`, or the overlay escapes upwards to whatever ancestor happens to be positioned. Scoping it that way keeps the rest of the screen scrollable and usable. `fullscreen` is fixed to the viewport and is what `HubLoadingService` mounts on `document.body`, outside every component subtree, so no ancestor's `overflow` or stacking context can clip it. Both modes paint the translucent scrim unless `backdrop` is turned off.
Examples:
Container Overlay
An overlay confined to one card by that card's `position: relative`, leaving the rest of the page scrollable. Toggle the backdrop to see the difference between a background refresh and work that invalidates what is on screen.
Order #4821
3 items · delivered 14 Aug 2026
- Subtotal
- 212.00 €
- Shipping
- 9.90 €
- VAT (21%)
- 26.70 €
- Total
- 248.60 €
Turn the backdrop off to keep the content readable underneath — useful when the work is a background refresh rather than something that invalidates what is on screen.
Code
Import:
Template:
Component:
Fullscreen Service
`HubLoadingService` driven from TypeScript. Two overlapping requests show why the reference counter matters: the first `hide()` leaves the overlay up because the second caller still holds a reference, and `update()` rewrites the message mid-flight.
hideAll() after 5 seconds no matter what. Nothing yet.
Code
Import:
Template:
Component:
Fullscreen inside a shell
A fullscreen indicator declared inside a side-panel container. Turn hoisting off and it still covers the window — the containment went in panels 22.15.0 — but it paints under the panel, because the container keeps a stacking context of its own. With the default `appendTo="body"` it leaves the subtree and covers everything.
Workspace
The indicator below is declared here, inside the container's content — the place a real application would write it.
Restoring the last session…
Three documents, two of them shared.
Last sync: 23 Sep 2026, 09:14.
With hoisting off, the overlay still covers the window — the containment went in 22.15.0 — but it paints under this panel, because the container keeps a stacking context of its own. Leaving the subtree is the only fix that does not depend on what an ancestor declares.
The overlay clears itself after a couple of seconds, and a click anywhere on it dismisses it at once.
Code
Import:
Template:
Component:
Branding And Theming
`image` swaps the built-in indicator for a mark of your own, animated with `none`, `spin` or `pulse`: `spin` only reads well on a radially symmetric mark, while `pulse` keeps a wordmark upright and legible. Everything visual lives in the `--hub-loading-*` tokens, declared on `:where(.hub-loading)` so any ancestor can set them — accent, size, speed, stroke thickness, gap, image size, text colour, and the backdrop tint and blur. The `hub-loading-theme()` Sass mixin sets the whole ladder in one call, and `provideHubLoading()` re-bases the defaults for an entire application without touching a template.
Examples:
Branded Image
A brand mark replacing the built-in indicator, shown with each of the three image animations so the trade-off between `spin`, `pulse` and `none` is visible.
imageAnimation="none"imageAnimation="spin"imageAnimation="pulse" Any URL works — an asset path, an imported file or, as here, an inline data URI. Size the mark with --hub-loading-image-size rather than size, which drives the built-in indicators.
Code
Import:
Template:
Component:
CSS Variable Theming
Live theming through the `--hub-loading-*` tokens set on a wrapper element, including the backdrop tint and blur that only do anything in `overlay` and `fullscreen` modes.
Overlay preview
The backdrop tint and blur only apply in overlay and fullscreen modes. The tint is mixed from --hub-sys-surface-page, so it veils this text with the theme's own surface instead of a fixed colour — drop it to zero and the card stays fully legible underneath.
The same overrides written once, in Sass, are in the CSS tab — that is what hub-loading-theme() is for.
Code
Import:
Template:
Component:
Page Progress Bar
`hub-loading-bar` is the thin strip that reports the page itself is on its way — the one under the navbar. Three decisions make it believable. It counts its callers, so a navigation and the three requests the page fires are four references and the bar finishes when the last one does, not the first. It waits before painting anything: work that finishes inside the grace period never shows a bar at all, because a 40 ms route that flashes a progress bar reads as a glitch rather than as speed. And its trickle decelerates as it fills, stopping short of the end — nothing here knows the real percentage, so only `complete()` may show 100%, because only `complete()` knows it is true. Place it with `mode="overlay"` inside a positioned navbar, or `mode="fixed"` and `--hub-loading-bar-offset` when the navbar is fixed too.
Examples:
Page Progress Bar
The bar hanging off a mock navbar. Three routes show the three behaviours: an ordinary one trickles, a slow one shows the trickle decelerating, and a 40 ms one never paints a bar at all.
Orders
This page arrived without you watching a spinner.
Code
Import:
Template:
Component:
Known And Unknown Progress
Binding `progress` hands the bar to the caller and publishes `aria-valuenow`; `indeterminate` sweeps instead and withholds it. The third state, `null`, puts the bar away.
Known percentage
[progress]="null"A real value, so the bar announces it. Reaching 100% is the caller's call, not the trickle's.
No percentage at all
indeterminateA sweep instead of a fill, for a stream or a job that reports no stages. No value is announced, because there is none.
Both bars are mode="inline", so each reserves its own row and nothing on the card moves when a bar appears or goes away.
Code
Import:
Template:
Component:
Router And HTTP Wiring
Why the bar counts its callers: a navigation plus three requests are four references, and the bar finishes when the last one does. The code shows `provideHubLoadingBarRouter()`, the interceptor and the opt-out.
Nothing yet.
Code
Import:
Template:
Component:
Sweeping Right to Left
The same `indeterminate` bar under `dir="ltr"` and under `dir="rtl"`. The sweep reverses with the text, driven by `--hub-loading-bar-sweep-direction`, so nothing has to be bound or duplicated per locale.
Loading results…
dir="ltr"Sweeps left to right, with the text.
جارٍ تحميل النتائج…
dir="rtl"يمسح من اليمين إلى اليسار، مع اتجاه النص.
The flip comes from [dir='rtl'] .hub-loading-bar, .hub-loading-bar[dir='rtl'], so the attribute counts wherever it sits — on <html>, on the region that changes direction, or on the <hub-loading-bar> element itself. The determinate fill needs no such treatment: it grows along the inline axis, which already reverses on its own.
Code
Import:
Template:
Component:
Key features
Recent changes
Version 22.4.1 - 9/28/26, 12:00 AM
changed: The family list in both READMEs names `ng-hub-ui-spreadsheet`. The sheet joined the family and the other lists had not caught up. Documentation only: no code, types or styles change.
Version 22.4.0 - 9/23/26, 12:00 AM
fixed: A mode="fullscreen" indicator covers the window from wherever it is written. position: fixed measures from the viewport only while no ancestor applies layout containment, a transform or a filter, and paints over the page only while no ancestor opens a stacking context. A shell breaks both routinely — <hub-side-panel-container> opens a stacking context on purpose, and any card with a transform moves the containing block — so an indicator declared inside one covered its own corner of the page at z-index: 1055 and nothing else. Applications were shipping a directive of their own to move the node to <body> on the screens where it showed.
added: appendTo on <hub-loading>: the CSS selector of the element a fullscreen indicator is re-parented to, 'body' by default. null leaves it where the template declares it, and a selector that matches nothing does the same rather than guessing another parent. inline and overlay are never moved. Switching away from fullscreen puts the indicator back between the siblings it came from, and destroying the view takes it off the page.
changed: BREAKING: a fullscreen indicator no longer hangs from the block that declares it. The component instance, its inputs and its outputs are untouched; the host element moves. A stylesheet that reached it through an ancestor selector, or a --hub-* token set on an ancestor rather than on :root, stops reaching it, and a test that looks for it under the fixture root will not find it. See BREAKING_CHANGES.md.
Version 22.3.0 - 9/23/26, 12:00 AM
changed: BREAKING: the Angular floor rises from 17.1.0 to 17.3.0. The old range was measured from the source alone, and its published `.d.ts` names `InputSignalWithTransform` or `OutputEmitterRef`, which Angular did not ship until 17.3. An application below the new floor could install this package and then fail to build; it now gets the peer warning it should always have had. Nothing that worked stops working. See BREAKING_CHANGES.md.
Version 22.2.2 - 9/23/26, 12:00 AM
changed: The Angular peer range now says what the code needs. It asked for >=21.0.0, which nothing in this package justified: the newest Angular API the source uses is input(), which shipped in 17.1, and the compiled output carries no linker marker above it. The range is >=17.1.0, and npm run peers:floors derives and checks that floor from now on.
Version 22.2.1 - 9/16/26, 12:00 AM
changed: Repository, issue and README links follow the move to the hub-env organization. Issues for every Hub UI package are now gathered in hub-env/hub-ui, and the repository and bugs fields of the manifest point at the new addresses. No code, types or styles change.
Version 22.2.0 - 9/8/26, 12:00 AM
changed: <hub-loading> and <hub-loading-bar> keep their stylesheets to themselves. Both shipped with ViewEncapsulation.None, and neither reason recorded for it held any more. Retheming from a global stylesheet is answered by the tokens, not by the encapsulation mode: they are declared on the host at zero specificity, a custom property inherits down to every element inside, and a rule written against .hub-loading still reaches the host because the class is on the host element. And the overlay HubLoadingService mounts on document.body is a real component instance created through createComponent(), so it carries its own stylesheet with it. See BREAKING_CHANGES.md.
changed: The bar's RTL rule is written as :host-context([dir='rtl']). It was a hand-written pair of selectors, one for the attribute on an ancestor and one for the attribute on the bar itself, because dir is inherited and both spellings are legitimate. :host-context() is exactly that pair, and under emulated encapsulation Angular now compiles it, which it never did while the component was unencapsulated. Same behaviour, one selector.
added: ng-hub-ui-ds is declared as an optional peer dependency (>=22.0.0). Both stylesheets have always resolved their defaults through the --hub-sys-* / --hub-ref-* ladder and the manifest said nothing about it, so a consumer reading the package on npm could not tell that installing the token package is what hands the indicator and the bar the family palette and its dark mode. It stays optional: every token carries a literal fallback and the library renders without it.
Version 22.1.1 - 9/6/26, 12:00 AM
added: Both READMEs now document the BEM classes of the loading bar and add a Right-to-left section explaining that the indeterminate sweep reverses under dir="rtl" so it travels with the text. Both have worked since 22.1.0 with nothing in the documentation to find them by, which is the same as not having them.
changed: rxjs is now declared as a peer dependency, because hubLoadingBarInterceptor imports finalize from it on the published path and a strict installer such as pnpm with hoisting turned off had no reason to resolve it. Nothing changes under npm or yarn, where rxjs arrives hoisted as a peer of @angular/core.
fixed: The Angular badge and the opening line of both READMEs say 21+, the range package.json has always declared. Claiming 22+ turned away an application on Angular 21 that the library supports.
fixed: HubLoadingService.isLoading is described as what it computes, the reference counter rather than the mounted overlay. The README contradicted itself, since its own SSR section already explained that on the server the counter runs while the mount is skipped.
fixed: FUNCTIONALITIES.md marks the ariaLabel of hub-loading as covered, which the Inline playground control has made true since the page shipped, and adds the RTL row of the bar so the file stops omitting behaviour the stylesheet has.
fixed: Calling HubLoadingService.hide(), HubLoadingBarService.complete() or HubLoadingBarService.inc() from inside an effect() no longer subscribes that effect to the internal state of the service. Each of those paths read a tracked signal to decide what to do next, the counter, whether the bar is on screen, the current fill, so the effect woke up on state it does not own: a show() from anywhere else re-ran it and retired a reference it never registered, and inc() re-entered itself on every step. The untracked read that already guarded the bar counter now covers the three places that were left out.
Version 22.1.0 - 9/5/26, 12:00 AM
added: hub-loading-bar: the thin page-progress strip that sits under the navbar, in three placements — inline in the flow, overlay against a positioned ancestor, or fixed to the viewport at --hub-loading-bar-offset.
added: HubLoadingBarService, with reference-counted start() / complete(), an anti-flicker grace period so fast work never paints a bar at all — set delay to 0 to opt out and reveal synchronously — and a trickle that decelerates as it fills and stops short of the end, since only complete() may show 100%.
added: provideHubLoadingBarRouter(), which runs the bar for the length of a navigation, including one a guard rejects, and hubLoadingBarInterceptor with withoutHubLoadingBar() to keep polls and heartbeats out of the count.
added: Determinate mode through the progress input, which publishes aria-valuenow, and an indeterminate sweep. Both withhold the value while the number is invented, which is how ARIA marks a progressbar of unknown position.
added: provideHubLoadingBar(), the HUB_LOADING_BAR_CONFIG token, the exported hubLoadingBarTrickle() curve, thirteen --hub-loading-bar-* tokens and the hub-loading-bar-theme() Sass mixin.
Version 22.0.0 - 8/24/26, 12:00 AM
added: Initial release of hub-loading: one component covering the inline block, the container overlay and the fullscreen layer, with role="status", aria-live="polite" and aria-busy set on the host.
added: Five pure-CSS indicators — spinner, dots, bars, pulse and ring — in three size steps, with no image or font dependency.
added: Accent resolution through resolveHubAccent() from ng-hub-ui-utils: a bare name becomes a --hub-sys-color-* token and follows the theme, while a hex, an oklch() or a var(...) is used verbatim.
added: Optional image input with none, spin and pulse animations, replacing the built-in indicator with a brand mark.
added: HubLoadingService for the fullscreen overlay, with a reference counter so concurrent callers cannot unblock the screen out from under each other, plus update() to re-dress the live overlay and hideAll() to force the count to zero. SSR-safe: a no-op without a DOM.
added: provideHubLoading() and the HUB_LOADING_CONFIG token, supplying application-wide defaults to both the component and the service.
added: Theming through the --hub-loading-* token ladder and the hub-loading-theme() Sass mixin, shipped at ng-hub-ui-loading/styles.
Related libraries
Frequently asked questions
How do I show a loading spinner or overlay in Angular?
Install ng-hub-ui-loading and render `<hub-loading>` while the work is in flight. It defaults to an inline block in the document flow; set `mode` to `overlay` to cover the surrounding container, or to `fullscreen` to block the viewport. For an application-wide overlay, inject `HubLoadingService` and call `show()` and `hide()` around the operation.
Should I use a loading spinner, a skeleton or a progress bar?
Reach for ng-hub-ui-skeleton when you already know the shape of the content that is coming, because the placeholder reserves the space and avoids layout shift. Use ng-hub-ui-loading when the wait is indeterminate or the screen has to be blocked — a form being submitted, a report being generated. When you can report real completion, ng-hub-ui-metrics renders determinate bars, meters and rings instead.
How do I change the colour of the Angular loading indicator?
Set the `color` input, which accepts a semantic name, a hex literal, an `oklch()` value or a `var(...)` reference to one of your own tokens. For a permanent change, every colour, size and duration is a `--hub-loading-*` custom property you can redefine from your own stylesheet, and the `hub-loading-theme()` Sass mixin restyles the whole component in one place. The `image` input replaces the indicator with a logo when the loading state should carry the brand.
Is the Angular loading component accessible and SSR-safe?
Yes. The host is a live region — `role=status`, `aria-live=polite` and `aria-busy` — with an `ariaLabel` input for the announced text, and every animation is disabled under `prefers-reduced-motion`. On the server there is no DOM to mount into, so `HubLoadingService` keeps only its counter and server-side rendering stays safe.
How do I add a page loading bar under the navbar in Angular?
Render `<hub-loading-bar mode="overlay" placement="bottom">` inside a navbar that carries `position: relative`, or use `mode="fixed"` with `--hub-loading-bar-offset` when the navbar is fixed too. Then add `provideHubLoadingBarRouter()` so the bar runs for the length of each navigation, including one a guard rejects, and `hubLoadingBarInterceptor` so it also covers the requests the page fires on arrival. The bar reference-counts its callers, so the two compose without either knowing about the other, and `withoutHubLoadingBar()` keeps background polls out of the count.