loadersz

August 29, 2026 · View on GitHub

npm version npm downloads CI bundle size single loader size motion states license TypeScript core dependencies

Framework-agnostic, dependency-free Canvas loaders for the browser. loadersz ships as a native Web Component and a small imperative TypeScript API-no React, SVG filters, or runtime dependencies.

150 motion states. One fixed state is 2.5–3.1 kB gzipped; the complete switchable library is available from one import.

Install

npm install loadersz

Enumerate available states

Use the tiny, side-effect-free states entry when building a picker or documentation UI:

import { LOADER_STATES } from "loadersz/states";

console.log(LOADER_STATES); // ["working", "searching", …]

Use the category metadata when building a picker or gallery. It keeps product-style loaders, loaders, systems, data & telemetry, reasoning, atmosphere and motion studies in the same order as the official website:

import { LOADER_CATEGORIES } from "loadersz/states";

for (const category of LOADER_CATEGORIES) {
  console.log(category.label, category.states);
}

Import once in your application to register the custom element safely:

import "loadersz";

Custom element

<loadersz-loader
  state="calibrating"
  size="144"
  speed="1.15"
  density="1"
  theme="dark"
  aria-label="Calibrating"
></loadersz-loader>

size is required in practice: it is the square canvas side in CSS pixels. Use 64 for a compact status indicator, 144 for a card, or 280 for a hero-sized loader.

Attributes

AttributeTypeDefaultDescription
statestate nameworkingSelects the visual geometry.
sizenumber96Square side in CSS pixels; values below 16 are clamped.
speednumber1Timeline multiplier.
densitynumber1Geometry detail; clamped to 0.352.
particle-radiusnumber1Multiplies visible particle thickness; clamped to 0.52.5.
themeauto, dark, lightautoCanvas colour scheme.
hue0360unsetOverrides a mode's native colour palette.
colorCSS colour stringunsetOverrides hue; accepts hex, CSS colours, and var(--token).
pausedboolean attributeunsetStops frame scheduling and keeps the current frame visible.
force-motionboolean attributeunsetOverrides a reduced-motion preference; use only when appropriate.
aria-labelstringLoadingAccessible label applied to the internal canvas.

Colour modes

Omit both hue and color to preserve the loader's native treatment. That can be a multi-colour palette or deliberately monochrome grey, depending on the movement. Set hue to recolour every visible particle with one hue, or set color for one exact CSS colour. color takes precedence over hue.

States

working, searching, connecting, weaving, shaping, listening, breathing, composing, solving, observing, dreaming, charging, flowing, awakening, coding, transcending, singing, growing, blooming, wandering, decoding, calibrating, attracting, shattering, crystallizing, constellating, folding, echoing, balancing, weathering, mapping, forging, orbiting, racing, bubbling, spinning, electrifying, flocking, throbbing, cascading, spiraling, juggling, eclipsing, resonating, condensing, dispersing, prisming, levitating, synchronizing, unraveling, pondering, deducing, branching, focusing, reflecting, weighing, recalling, tracing, converging, questioning, glimmering, radiating, harmonizing, twinkling, flickering, shimmering, surfacing, vibrating, illuminating, sparkling, loading, buffering, typing, processing, synthesizing, considering, uploading, queuing, associating, evaluating, reasoning, exploring, linking, resolving, and imagining.

Framework adapters

The core package remains a native Web Component. Typed adapters are available for the three frameworks that need framework-specific TypeScript or component integration.

React

import { LoaderszLoader } from "loadersz/react";

export function Status() {
  return (
    <LoaderszLoader
      state="racing"
      size={144}
      speed={1.2}
      ariaLabel="Loading results"
    />
  );
}

Importing loadersz/react also adds type support for direct custom-element JSX:

import "loadersz/react";

const loader = <loadersz-loader state="orbiting" size={96} />;

Vue 3

<script setup lang="ts">
import { LoaderszLoader } from "loadersz/vue";
</script>

<template>
  <LoaderszLoader
    state="bubbling"
    :size="144"
    :speed="1.1"
    aria-label="Loading"
  />
</template>

Svelte 4 and 5

<script lang="ts">
  import 'loadersz/svelte';
</script>

<loadersz-loader state="spiraling" size={144} aria-label="Loading" />

Angular

Use the standalone Angular directive. It makes the native element known to Angular and adds typed bindings without CUSTOM_ELEMENTS_SCHEMA. The runtime import is intentionally explicit, so the same pattern works with a small direct state import. Keep the loader in normal HTML — never inside an <svg> or <defs> block.

import { Component } from "@angular/core";
import "loadersz";
import { LoaderszLoader } from "loadersz/angular";

@Component({
  standalone: true,
  selector: "app-root",
  imports: [LoaderszLoader],
  template: `
    <loadersz-loader
      [state]="state"
      [size]="144"
      aria-label="Loading"
    ></loadersz-loader>
  `,
})
export class App {
  readonly state = "racing" as const;
}

For Solid, Qwik, Lit, and plain HTML, import loadersz and use <loadersz-loader> directly.

Which import should I use?

FrameworkLoader can change at runtimeOne fixed loader
Reactimport { LoaderszLoader } from 'loadersz/react'import 'loadersz/racing' plus React's type-only import below
Vue 3import { LoaderszLoader } from 'loadersz/vue'import 'loadersz/racing' and use the native element
Svelteimport 'loadersz/svelte'import 'loadersz/racing' plus import type {} from 'loadersz/svelte'
Angularimport 'loadersz' + LoaderszLoaderimport 'loadersz/racing' + LoaderszLoader
Lit, Solid, Qwik, HTMLimport 'loadersz'import 'loadersz/racing'

The controls are always the same. The native element spells its multi-word attributes as aria-label, force-motion, and particle-radius; React and Vue wrappers spell them as ariaLabel, forceMotion, and particleRadius. state, size, speed, density, hue, color, theme, and paused keep the same name.

Single-mode imports

When a loader will never change state, use its direct entry point. It includes the Canvas core and exactly one geometry builder-no tree-shaking configuration required.

import "loadersz/racing";
<loadersz-loader
  state="racing"
  size="144"
  speed="1.15"
  aria-label="Loading results"
></loadersz-loader>

The direct entry's visual state is fixed by its import. The state attribute is accepted so markup stays familiar, but cannot load the other 84 animations. Every state name has a matching entry point: loadersz/working, loadersz/solving, loadersz/racing, and so on.

In React, a direct entry can stay small too. Import the fixed runtime entry and the adapter types only; TypeScript removes the type-only import from the browser bundle.

import "loadersz/racing";
import type { LoaderszReactProps } from "loadersz/react";

const loaderProps = { state: "racing", size: 96 } satisfies LoaderszReactProps;

export function Loading() {
  return <loadersz-loader {...loaderProps} aria-label="Loading" />;
}

The direct import itself is framework-agnostic: it always registers the same native element and supports the same state, size, speed, density, hue, color, theme, paused, and force-motion attributes. Framework wrapper types are intentionally specific to their framework: use LoaderszReactProps for React, loadersz/vue for the typed Vue component, and import type {} from 'loadersz/svelte' alongside a direct entry for Svelte template typing. The Vue wrapper imports the complete switchable component, so use the native element when the smallest single-state bundle matters.

For a fixed canvas you own yourself, the same entry point exposes a fixed imperative controller:

import { LoaderszLoader } from "loadersz/racing";

const canvas = document.querySelector<HTMLCanvasElement>("#loading");
if (!canvas) throw new Error("Missing canvas");

const loader = new LoaderszLoader(canvas, {
  size: 144,
  speed: 1.15,
  ariaLabel: "Loading results",
});

Use the root import when a component must change between states at runtime:

import "loadersz";

loadersz/modes remains available for applications that want named fixed-mode factories and already rely on an ESM-aware production bundler:

import { racing } from "loadersz/modes";

const canvas = document.querySelector<HTMLCanvasElement>("#loading");
if (!canvas) throw new Error("Missing canvas");

const loader = racing(canvas, {
  size: 144,
  speed: 1.15,
  ariaLabel: "Loading results",
});

The returned controller still supports setOptions, paused, colour, speed, density, and destroy(). Its visual mode is fixed to the imported factory.

Imperative API

Use the imperative API when your application owns a canvas or controls the lifecycle directly.

import { LoaderszLoader } from "loadersz";

const canvas = document.querySelector<HTMLCanvasElement>("#loader");
if (!canvas) throw new Error("Missing canvas");

const loader = new LoaderszLoader(canvas, {
  state: "solving",
  size: 160,
  speed: 1.1,
  theme: "dark",
  ariaLabel: "Solving",
});

loader.setOptions({ state: "forging", size: 200, hue: 28 });
loader.destroy();

All exported types and methods include TSDoc, so editors show parameter descriptions, defaults, return values, and lifecycle behaviour on hover.

Accessibility and performance

  • Respects prefers-reduced-motion by default and pauses in hidden tabs.
  • Canvas device-pixel ratio is capped at 2 to avoid excessive bitmap work.
  • The core has zero regular runtime dependencies. React, Vue, and Svelte are optional peer dependencies and are only needed when importing their adapter entry point.
  • Use a meaningful aria-label for non-decorative loaders. Use aria-hidden="true" on a decorative surrounding element where appropriate.

Development

Install dependencies and enable the local Git hooks once:

npm install
npm run hooks:install

Commits follow Conventional Commits. Pre-commit checks format and lint staged files. The release package intentionally contains no install lifecycle scripts.

Browser benchmark UI

Start the interactive production-build benchmark:

npm run benchmark:ui

Open http://127.0.0.1:4174, choose a scenario, and select Run benchmark. Stop the local server with Ctrl+C. See BENCHMARKING.md for A/B comparisons, equivalence checks, CI behaviour, and measurement rules.

License

MIT © 2026 lumberjacque.