npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

loader-buttons

v0.1.0

Published

25 original loading button animations built with WebGL, Canvas, SVG and CSS — headless loaders, a styled button, and a React component. Zero runtime dependencies.

Readme

loader-buttons

25 original loading-button animations built with WebGL 2, Canvas, SVG and CSS — as a headless loader runtime, a styled button, and a React component.

No runtime dependencies. No build step. Ships as plain ESM.

Live gallery →

Install

npm install loader-buttons

Quick start

React

import { LoaderButton } from "loader-buttons/react";
import "loader-buttons/styles.css";

function SaveButton({ isSaving, onSave }) {
  return (
    <LoaderButton
      design="mercury-relay"
      label="Saving"
      idleLabel="Save changes"
      loading={isSaving}
      onClick={onSave}
    />
  );
}

Vanilla

import { createLoaderButton } from "loader-buttons";
import "loader-buttons/styles.css";

const button = createLoaderButton(document.querySelector("#toolbar"), {
  design: "mercury-relay",
  label: "Saving",
  onClick: () => save(),
});

button.setLoading(false);

Headless

Mount an animation into an element you already own — no styles, no markup opinions, just the frame loop and lifecycle management.

import { mountLoader } from "loader-buttons";

const loader = mountLoader(document.querySelector("#spinner"), {
  design: "voronoi-lantern",
  speed: 1.5,
});

loader.pause();
loader.seek(2.4);
loader.destroy();

What the runtime handles for you

  • One frame loop for the whole page. Every loader on a shared clock renders from a single requestAnimationFrame, throttled to a frame budget you choose.
  • Nothing runs off-screen. An IntersectionObserver mounts loaders lazily and freezes them when they scroll away. The clock stops entirely when no loader needs it, so an idle page costs nothing.
  • One WebGL context, shared. All 13 shader loaders draw through a single WebGL 2 context. Programs are freed when a loader stays off-screen, and each shader carries a hand-written Canvas 2D fallback for machines without WebGL 2.
  • Sharp on every display. Device-pixel-ratio changes — a monitor swap, a browser zoom — trigger a re-rasterise, capped at a density you set.
  • Reduced motion, honoured. Loaders freeze for visitors who ask for it.
  • Hidden tabs pause. No frames are drawn in a background tab.

Choosing a design

import { designs, designsByKind, getDesign } from "loader-buttons";

designs.length;                 // 25
designs[0].id;                  // "fibonacci-breather"
designsByKind("webgl").length;  // 13
getDesign("mercury-relay").name; // "Mercury Relay"

Pass a design as an id string, a design object, or a numeric index.

| # | id | kind | # | id | kind | |---|----|------|---|----|------| | 01 | fibonacci-breather | webgl | 14 | conic-eclipse | dom | | 02 | glass-tide-lens | webgl | 15 | orbital-calligraphy | svg | | 03 | binary-metaball-mitosis | webgl | 16 | mercury-relay | svg | | 04 | curl-field-comet | webgl | 17 | noise-chrysalis | svg | | 05 | living-reaction-seed | webgl | 18 | contour-choir | svg | | 06 | chladni-whisper | webgl | 19 | shapeshifter-seal | svg | | 07 | topology-courier | webgl | 20 | murmuration-turn | canvas | | 08 | meridian-loom | webgl | 21 | frostwork-growth | canvas | | 09 | voronoi-lantern | webgl | 22 | sandpile-bloom | canvas | | 10 | event-horizon-pulse | webgl | 23 | elastic-cartography | canvas | | 11 | prismatic-caustic-coin | webgl | 24 | mechanical-iris | svg | | 12 | voxel-assembly-line | webgl | 25 | braille-flipwave | dom | | 13 | strange-attractor-scribe | webgl | | | |

Import one design on its own if you do not want the rest in your bundle:

import mercuryRelay from "loader-buttons/designs/16-mercury-relay.js";

Button options

Every option below works on the React component as a prop, on createLoaderButton as an option, and on button.update({ ... }) afterwards.

Content and state

| Option | Type | Default | | |---|---|---|---| | design | id, object, or index | — | Required. Which animation to run | | label | string | "Working" | Text shown while loading | | idleLabel | string | falls back to label | Text shown when loading is false | | loading | boolean | true | Collapses and freezes the animation when false | | disabled | boolean | false | | | type | "button", "submit", "reset" | "button" | | | ariaLabel | string | design name when there is no label | |

Appearance

| Option | Type | Default | | |---|---|---|---| | size | "sm", "md", "lg" | "md" | 114×42, 150×58, 186×72 | | variant | "glass", "solid", "outline", "ghost" | "glass" | | | theme | "light", "dark", "auto" | "light" | "auto" follows prefers-color-scheme | | shape | "pill", "rounded", "square" | "pill" | | | iconPosition | "start", "end" | "start" | | | fullWidth | boolean | false | | | responsive | boolean | true | Drops one size step below 690px |

Chrome and motion

| Option | Type | Default | | |---|---|---|---| | tilt | boolean | true | 3D press tilt | | sheen | boolean | true | Diagonal highlight sweep on hover | | rim | boolean | true | Conic rim light | | press | boolean | true | Scale down on press | | resetOnClick | boolean | true | Restart the animation on click | | resetOnLoading | boolean | true | Restart when loading flips back on | | replayDuration | number | 140 | Milliseconds the press animation lasts |

Design tokens

Each of these sets one CSS custom property. Numbers are read as pixels where a length is expected.

width · height · radius · slotSize · gap · paddingInline · fontFamily · fontSize · fontWeight · letterSpacing · lineHeight · ink · labelColor · surface · borderColor · borderWidth · blur · shadow · focusRing · pressScale · tiltAngle · ease · duration

<LoaderButton
  design="event-horizon-pulse"
  label="Rendering"
  shape="rounded"
  width={210}
  ink="#c0392b"
  labelColor="#c0392b"
  speed={1.6}
/>

ink is the colour the animation itself draws with.

Playback

| Option | Type | Default | | |---|---|---|---| | speed | number | 1 | 0 freezes, 2 runs at double speed | | phaseOffset | number | 0 | Seconds the animation starts and restarts from | | paused | boolean | false | | | fps | number | 30 | Frame budget, clamped to 1–120 | | clock | clock | shared clock for this fps | Share one loop across components | | maxDpr | number | 3 | Upper bound on rendered pixel density | | pauseOffscreen | boolean | true | | | respectReducedMotion | boolean | true | | | releaseWebGLOffscreen | boolean | true | | | releaseDelay | number | 1200 | Milliseconds off-screen before WebGL is freed | | rootMargin | string | "120px 0px" | | | threshold | number | 0.01 | |

Callbacks

onClick(event) · onError(error, { design, stage }) · onFrame(time, delta)

Theming with CSS

Every visual decision is a --lb-* custom property. Override them anywhere in the cascade:

.lb-loader-button {
  --lb-width: 200px;
  --lb-radius: 12px;
  --lb-ink: #0f62fe;
  --lb-label-color: #0f62fe;
  --lb-surface: rgba(255, 255, 255, 0.7);
}

The stylesheet declares its own box model and never adds a global reset, so it will not collide with your design system.

API

createLoaderButton(target?, options)

Returns an instance with element, button, slot, labelElement, loader, design, loading, disabled, and options, plus:

update(options) · setDesign · setLabel · setLoading · setDisabled · setVariant · setSize · setTheme · setSpeed · reset() · play() · pause() · destroy()

Pass the options object alone to get a detached element you place yourself. options.mount chooses "append" (default), "prepend", or "replace".

mountLoader(element, options)

Returns a loader instance with design, time, speed, paused, visible, animating, reducedMotion, failed, destroyed, and clock, plus:

play() · pause() · toggle() · reset() · seek(seconds) · renderFrame() · setSpeed · setPhaseOffset · setMaxDpr · setDesign · refresh() · destroy()

Every mutator returns the instance, so calls chain.

useLoader(elementRef, options)

The React hook behind the component. Drive an animation inside markup you render yourself:

function Spinner() {
  const ref = useRef(null);
  const loader = useLoader(ref, { design: "chladni-whisper", speed: 0.8 });
  return <span ref={ref} style={{ width: 40, height: 40 }} />;
}

createClock(options) / getSharedClock(fps)

import { createClock } from "loader-buttons";

const clock = createClock({ fps: 60 });
clock.stop();  // freeze every loader on this clock at once
clock.start();

getSharedClock(fps) returns the process-wide clock for a frame budget, which is what loaders use when you do not pass one.

Authoring your own loader

A design is one object. Nothing else is required:

export default {
  id: "my-loader",
  name: "My Loader",
  kind: "canvas",
  technique: "Short implementation label",
  label: "Working",
  mount(container) {
    return {
      render(timeSeconds, deltaSeconds) {},
      resize(widthCssPixels, heightCssPixels, devicePixelRatio) {},
      reset() {},
      destroy() {},
    };
  },
};

Pass it anywhere a design id is accepted. The runtime hands mount a disposable host element of its own, so you may attach a shadow root to it.

Never start your own requestAnimationFrame, interval, or timeout — the clock owns time, which is what makes off-screen pausing and deterministic seeking work. createCanvasVisual and createShaderVisual are exported for Canvas 2D and fragment-shader loaders respectively.

Browser support

Chrome, Edge, Firefox, and Safari 16.4+. WebGL 2 loaders fall back to Canvas 2D where WebGL 2 is unavailable. mountLoader and createLoaderButton throw outside a browser; importing the package on a server is safe, so the React component server-renders its markup and mounts the animation on the client.

Repository

https://github.com/Appllama/25-unique-loaders — the package lives in packages/loader-buttons/, alongside the gallery site that consumes it.

License

Apache-2.0