Loader
Loader is a compact loading indicator, shown as a three-dot bounce, a rotating ring, or a horizontal bar, all rendered entirely with CSS, with no image asset or animation loop involved. The dots and spinner variants automatically scale with, and adopt the color of, whatever they're placed inside, unless overridden. The bar variant runs indeterminately by default, or switches to a determinate, measured fill, and can be pinned in place while scrolling.
When to use
Reach for dots or spinner for a small, in-place "this is working"
indicator, such as inside a button while it submits, next to a field
while it validates, or in a table cell while a value loads. Reach for
bar for a wider, section or page-level indicator, such as above a page
while it loads or atop a panel while its content refreshes, indeterminate
by default, or determinate once there's actual progress to report, such
as an upload or a multi-step process. None of the three are built for a
whole page or route's first load. For that, show
Install & usage
Pick a framework in the toolbar above and these snippets adapt.
npm install @eunomia/elements
import "@eunomia/elements/loader.js";
<eun-loader></eun-loader> <eun-loader variant="spinner"></eun-loader>
Importing the file registers <eun-loader> as a custom element, with no
further setup needed. It works with any framework, or none, since it's a
standard web component.
npm install @eunomia/elements
<script type="module">
import "@eunomia/elements/loader.js";
</script>
<eun-loader></eun-loader>
npm install @eunomia/elements
import "@eunomia/elements/loader.js";
function Spinner() {
return <eun-loader variant="spinner" aria-label="Loading results" />;
}
npm install @eunomia/elements
"use client";
import "@eunomia/elements/loader.js";
export function Spinner() {
return <eun-loader variant="spinner" />;
}
npm install @eunomia/elements
<script setup>
import "@eunomia/elements/loader.js";
</script>
<template>
<eun-loader variant="spinner" />
</template>
npm install @eunomia/elements
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
import "@eunomia/elements/loader.js";
@Component({
selector: "app-spinner",
template: `<eun-loader variant="spinner"></eun-loader>`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class SpinnerComponent {}
Alternatives
eun-skeletoneun-progress-barGuidance
- Use
dots(the default) for a compact, embedded context: inside a button, a badge, a table cell - Use
spinneras a standalone or field-level indicator: inside a search/select field, next to a section heading, centered in an empty panel while its content loads - Use
barfor a section- or page-wide indicator: leavevalueunset while duration is unknown, and set it once there's real progress to report - Reserve
stickyfor a genuinely page-levelbar(e.g. pinned under a fixed header, via the defaultstickyPosition="top", or above a fixed footer/toolbar withstickyPosition="bottom"), since a bar embedded in a panel or card should scroll with its content like anything else in it - Let it inherit its surrounding font-size/color rather than setting
sizeunless the context genuinely needs a different scale - Set a specific
labeldescribing what's loading ("Loading results…") whenever the generic default wouldn't be clear from context
- Using
eun-loaderas a full-page/route-level loading state: it's an inline indicator, not a page skeleton - Leaving it on screen indefinitely on an error: swap it for an error state once a request settles, success or failure
- Using
barfor a scalar measurement that's never actually indeterminate: see Alternatives above
Live testing
Properties
Loader <eun-loader>
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| label | string | 'Loading…' | The accessible label announced to assistive technology |
| variant | 'dots' | 'spinner' | 'bar' | 'dots' | The visual style to render |
| size | string | — | The loader size, in any valid CSS unit. A bare number is treated as pixels. Unset, it inherits the surrounding font size |
| value | number | — | For the bar variant, the current loading percentage from 0 to 100, switching it to determinate mode. Left unset, bar animates indeterminately. Ignored by dots and spinner |
| sticky | boolean | false | For the bar variant, pins it to an edge of its nearest scrolling ancestor. Ignored by dots and spinner |
| sticky-position | 'top' | 'bottom' | 'top' | For the bar variant, while sticky is set, which edge it pins to. Ignored when sticky is unset, or by dots and spinner |
Import the exact TypeScript type behind any property above, see
CSS custom properties
| Name | Description |
|---|---|
| --loader-size | An alias for the size property, set as a CSS custom property instead |
| --loader-bar-track-height | Sets the height of the bar variant's track |
| --loader-bar-track-color | Sets the color of the bar variant's track |
| --loader-bar-fill-color | Sets the color of the bar variant's fill, defaulting to the surrounding text color |
| --loader-bar-border-radius | Sets the corner radius of the bar variant's track and fill |
| --loader-bar-transition-duration | Sets the duration of the bar variant's determinate fill transition |
| --loader-bar-sticky-offset | While sticky, sets the offset from the pinned edge of the scroll container |
| --loader-bar-z-index | While sticky, sets the stacking order |
Dots (default)
Used internally by eun-buttonloading state: compact enough to sit inside a button, badge, or table
cell without changing its surrounding layout.
<eun-loader></eun-loader>
Spinner
A rotating ring, meant for a standalone or field-level indicator, e.g. the
loading state on eun-searcheun-select
<eun-loader variant="spinner"></eun-loader>
Inherits color and size
Both variants use currentColor and em-relative sizing by default, so they
automatically match whatever text color/font-size surrounds them.
<div style="color: #148cd2; font-size: 24px;">
<eun-loader></eun-loader>
<eun-loader variant="spinner"></eun-loader>
</div>
Custom size
Set size to override the inherited font-size directly, without touching
the surrounding text.
<eun-loader size="32"></eun-loader>
<eun-loader variant="spinner" size="32"></eun-loader>
Bar
A full-width variant for a section- or page-level indicator, indeterminate by default or switched to a determinate fill, optionally pinned in place while its surroundings scroll.
Indeterminate
Left with no value, bar sweeps continuously: a full-width "work is
happening, duration unknown" indicator.
<eun-loader variant="bar" label="Loading results…"></eun-loader>
Determinate
Set value (0–100) to switch it to a fixed-width fill instead: an upload, a
multi-step process, anything with real, measurable progress.
<eun-loader variant="bar" value="25" label="Uploading…"></eun-loader>
Sticky
Set sticky to pin the bar to an edge of its nearest scrolling ancestor via
position: sticky, for a page-level bar that stays visible while the page
scrolls underneath it. stickyPosition picks which edge : top (the
default) or bottom.
Scroll this box: the bar stays pinned to its top edge.
More content to make the box scrollable, so the sticky behavior is visible as you scroll down past this line and the next few paragraphs below it.
Still scrolling: the bar above should remain fixed to the top of this bordered box the whole time.
One more line to make sure there's enough height to scroll through.
<div class="scroll-container">
<eun-loader variant="bar" sticky label="Loading feed…"></eun-loader>
<!-- scrolling content -->
</div>
Set sticky-position="bottom" to pin it to the bottom edge instead, e.g.
above a fixed footer or toolbar.
Scroll this box: the bar stays pinned to its bottom edge.
More content to make the box scrollable, so the sticky behavior is visible as you scroll down past this line and the next few paragraphs below it.
Still scrolling: the bar below should remain fixed to the bottom of this bordered box the whole time.
One more line to make sure there's enough height to scroll through.
<div class="scroll-container">
<!-- scrolling content -->
<eun-loader
variant="bar"
sticky
sticky-position="bottom"
label="Loading feed…"
></eun-loader>
</div>
Custom styling
Override the --loader-bar-* CSS variables: full list in the API tab.
<eun-loader
variant="bar"
value="55"
label="Custom styled bar"
class="custom-bar"
></eun-loader>
.custom-bar {
--loader-bar-track-height: 14px;
--loader-bar-track-color: #ede9fe;
--loader-bar-fill-color: #7c3aed;
--loader-bar-border-radius: 4px;
}
Aria attributes
dots/spinner render role="status" with aria-label (from the label
property, default "Loading…"): a screen reader announces it as a live,
polite status update rather than silently rendering a purely visual
animation with no accessible meaning. role="status" implies an
aria-live="polite" region on its own, per the ARIA specification, so no
extra live-region wiring is needed around it.
bar renders role="progressbar" instead, with aria-label,
aria-valuemin="0", and aria-valuemax="100" always present. In
indeterminate mode (no value set), aria-valuenow is intentionally
omitted: per the ARIA specification, a progressbar with no
aria-valuenow is itself the signal that progress is indeterminate, the
same way it's communicated visually by the sweeping fill. Once value is
set, aria-valuenow is added (clamped to 0–100), switching both the visual
fill and the accessible state to determinate together.
Dynamic updates aren't announced on their own
role="progressbar" doesn't imply a live region the way role="status"
does: a screen reader won't automatically re-announce bar's value as it
changes. If the progress needs to be heard as it updates (a multi-step
upload, for instance), pair it with your own visually-hidden
aria-live="polite" region that you update alongside value, rather than
relying on the bar itself.
<eun-loader variant="bar" value="40" label="Uploading…"></eun-loader>
<span class="sr-only" aria-live="polite">40% uploaded</span>
Motion
Every variant keeps animating under prefers-reduced-motion: reduce, just
significantly slower, rather than freezing solid: a loading indicator
communicates a real, ongoing state change, and a fully static spinner, dots
row, or indeterminate bar would read as broken/stuck rather than "still
loading", worse for a reduced-motion user than no indicator at all. A
determinate bar's width transition, on the other hand, isn't itself the
"is it working" signal: the fill's actual position already communicates
that on every render, so it isn't slowed under reduced motion.
Reference links
status role
progressbar role