Eunomia v1.0.0-beta.2
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

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.

Overview API Examples Accessibility

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 skeleton placeholders instead, so the layout doesn't jump once the real content arrives. For a scalar measurement within a known range that's never an unknown duration, such as disk usage, a score, or a battery level, reach for progress bar instead, since it has no indeterminate state. See Alternatives below for a side by side comparison.

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.

Alternatives

You want .. Prefers A whole page or route's first load, so the layout doesn't jump once the real content arrives eun-skeleton A scalar measurement within a known range that's never an unknown duration, such as disk usage or a score eun-progress-bar

Guidance

  • Use dots (the default) for a compact, embedded context: inside a button, a badge, a table cell
  • Use spinner as 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 bar for a section- or page-wide indicator: leave value unset while duration is unknown, and set it once there's real progress to report
  • Reserve sticky for a genuinely page-level bar (e.g. pinned under a fixed header, via the default stickyPosition="top", or above a fixed footer/toolbar with stickyPosition="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 size unless the context genuinely needs a different scale
  • Set a specific label describing what's loading ("Loading results…") whenever the generic default wouldn't be clear from context
  • Using eun-loader as 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 bar for a scalar measurement that's never actually indeterminate: see Alternatives above

Live testing

Properties

Loader <eun-loader>

Attributes

NameTypeDefaultDescription
labelstring'Loading…'The accessible label announced to assistive technology
variant'dots' | 'spinner' | 'bar''dots'The visual style to render
sizestringThe loader size, in any valid CSS unit. A bare number is treated as pixels. Unset, it inherits the surrounding font size
valuenumberFor 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
stickybooleanfalseFor 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 Types.

CSS custom properties

NameDescription
--loader-sizeAn alias for the size property, set as a CSS custom property instead
--loader-bar-track-heightSets the height of the bar variant's track
--loader-bar-track-colorSets the color of the bar variant's track
--loader-bar-fill-colorSets the color of the bar variant's fill, defaulting to the surrounding text color
--loader-bar-border-radiusSets the corner radius of the bar variant's track and fill
--loader-bar-transition-durationSets the duration of the bar variant's determinate fill transition
--loader-bar-sticky-offsetWhile sticky, sets the offset from the pinned edge of the scroll container
--loader-bar-z-indexWhile sticky, sets the stacking order

Dots (default)

Used internally by eun-button's own loading 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-search and eun-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

WAI-ARIA: status role
WAI-ARIA: progressbar role
WCAG 2.1: Animation from Interactions