Catalog npm
PACKAGE EXPLORERView in registry

3d-spinner-react

npm

React bindings for 3d-spinner: a lightweight 3D spinner, loader, and progress indicator as a component and a hook.

3dhookloaderloadingprogressreactreact-componentspinner
Versions collected8
Direct dependencies6From release 1.1.0
Last observed20 Oct, 19:37 UTC

README

Current package documentation

3d-spinner-react

tests npm license

React bindings for 3d-spinner - a zero-dependency 3D spinner, loader, and progress indicator that draws to a canvas. You get a <Spinner> component and a useSpinner hook for mounting, cleanup, and progress. Import the animations and prefabs you need from 3d-spinner; only what you import ends up in your bundle.

Live demo - the prefabs and common React patterns, each shown with the complete component it runs.

Install

npm install 3d-spinner-react 3d-spinner react

3d-spinner (1.1 or later) and react (18+) are peer dependencies.

Screenshots

Some ready-made prefabs from 3d-spinner/prefabs, shown here through <Spinner>:

starSwarm with custom HTMLpulsingStarfieldchargedOrb at 74%
starSwarm prefab with a custom HTML labelpulsingStarfield prefab with a Preparing launch labelchargedOrb progress prefab at 74 percent
ghostTrain progress prefabParticlesAnimation with glow texture
ghostTrain progress prefab on a tilted trackparticle loader with a Just a sec label

Prefabs

3d-spinner/prefabs ships complete setups - layered animation, labels, and sensible defaults already picked. Import one, pass its animation into <Spinner>, and give the container a width and height.

Pass animation as a factory (() => prefab().animation), not a bare instance. Each mount gets a fresh animation - that is what keeps things working under StrictMode and when props change. 3d-spinner throws if the same animation instance is mounted twice.

Indeterminate prefab:

import { Spinner } from "3d-spinner-react";
import { starSwarm } from "3d-spinner/prefabs";

function Loading() {
  return (
    <Spinner
      type="indeterminate"
      animation={() => starSwarm({ label: "Just a sec" }).animation}
      style={{ width: 240, height: 240 }}
    />
  );
}

Progress prefab - set progress from state; at 1 the outro plays:

import { useState } from "react";
import { Spinner } from "3d-spinner-react";
import { chargedOrb } from "3d-spinner/prefabs";

function Upload() {
  const [progress, setProgress] = useState(0.74);
  return (
    <Spinner
      progress={progress}
      animation={() => chargedOrb({ label: "Uploading" }).animation}
      style={{ width: 240, height: 240 }}
    />
  );
}

You can override backend, label, and fadeLabel on the prefab. Labels fade with the intro and outro by default; set fadeLabel: false to keep one fully visible. Motion prefabs also take object and particles options. A label can be plain text or an HTMLElement. If your factory closes over a prop, add it to deps so the spinner rebuilds when it changes.

Only the prefab's animation reaches <Spinner>, so spinner options such as periodMs, loop, timeoutMs, and ariaLabel go on <Spinner> itself.

import { Spinner } from "3d-spinner-react";
import { planeStarTrail } from "3d-spinner/prefabs";

function LaunchPreview({ rate }: { rate: number }) {
  return (
    <Spinner
      type="indeterminate"
      animation={() =>
        planeStarTrail({ label: "Preparing preview", particles: { rate } }).animation
      }
      deps={[rate]}
      style={{ width: 280, height: 280 }}
    />
  );
}

Available prefabs (each at 3d-spinner/prefabs):

ExportModeDescription
starSwarmindeterminateBright star particles around a centered message.
pulsingStarfieldindeterminateHigh-shine particles around a slowly pulsing label.
planeStarTrailindeterminateA small plane looping through a star-particle stream.
monochromeStreakindeterminateMinimal streak motion with particles.
crystalCometindeterminateComet with a particle tail.
rocketLaunchprogressRocket launch with exhaust particles.
chargedOrbprogressOrb that grows satellites as progress climbs.
ghostTrainprogressIce-cube train on a tilted track with a star trail.
gridAssemblyprogressGrid pieces assemble as progress advances.

See 3d-spinner's docs for how prefabs are built, plus custom animations, shapes, motion paths, and rendering backends.

Quick start

An indeterminate spinner runs until you hide it. Give the container a width and height - the canvas fills that box.

import { Spinner } from "3d-spinner-react";
import { SpinAnimation } from "3d-spinner/animations/spin";

function Loading({ loading }: { loading: boolean }) {
  return (
    <Spinner
      show={loading}
      type="indeterminate"
      animation={() => new SpinAnimation({ color: "#3b82f6" })}
      style={{ width: 120, height: 120 }}
    />
  );
}

When show turns false, the spinner plays its outro and then removes itself.

Ending with the outro

Unmounting a spinner stops it at once, which is what you want for a fast page transition but looks abrupt otherwise. To let it finish gracefully, keep <Spinner> rendered and switch show instead:

You writeWhat happens
{loading && <Spinner ... />}The spinner vanishes the moment loading turns false.
<Spinner show={loading} ... />The outro plays first, then the host div is removed.

onFinish runs when the outro has finished, just before the div is removed, so you can move on only after the animation ends. It also runs when a progress spinner reaches 1 and its finale has played:

<Spinner
  progress={progress}
  animation={() => chargedOrb().animation}
  onFinish={() => navigate("/results")}
  style={{ width: 240, height: 240 }}
/>

Showing it again mounts a fresh spinner with its intro. With the hook, you remove the element yourself, so wait for the outro with the promise that stop() returns:

await spinner.stop(); // resolves once the outro has finished
setVisible(false);

Reporting progress

Leave out type (it defaults to "progress") and update progress from state. Changes call setProgress without rebuilding the spinner; at 1 the outro plays. Add progressAnimation to SpinAnimation if you want it to pop in and scale with progress.

import { useState } from "react";
import { Spinner } from "3d-spinner-react";
import { SpinAnimation } from "3d-spinner/animations/spin";

function Upload() {
  const [progress, setProgress] = useState(0);
  return (
    <Spinner
      progress={progress}
      animation={() => new SpinAnimation({ progressAnimation: {} })}
      style={{ width: 160, height: 160 }}
    />
  );
}

Choosing a shape or animation

This wrapper does not bundle the 3D engine - import shapes and animations from 3d-spinner yourself.

import { Spinner } from "3d-spinner-react";
import { SpinAnimation } from "3d-spinner/animations/spin";
import { tetrahedron } from "3d-spinner/engines/little-3d-engine";

function Themed({ color }: { color: string }) {
  return (
    <Spinner
      type="indeterminate"
      animation={() => new SpinAnimation({ shape: tetrahedron(), color })}
      deps={[color]}
      style={{ width: 120, height: 120 }}
    />
  );
}

Each animation is imported from its own subpath:

ImportClassDescription
3d-spinner/animations/spinSpinAnimationA spinning 3D shape, a cube by default.
3d-spinner/animations/object-motionObjectMotionAnimationA mesh that follows a motion path, with an intro/outro you choose.
3d-spinner/animations/particlesParticlesAnimationCamera-facing billboard particles: burst, fountain, snow, confetti.
3d-spinner/animations/charged-orbChargedOrbAnimationProgress-driven orb with satellite pop-outs.
3d-spinner/animations/ghost-trainGhostTrainAnimationProgress story: translucent train gains a car per 2%, then blasts off.
3d-spinner/animations/grid-assemblyGridAssemblyAnimationGrid pieces that assemble with progress.
3d-spinner/animations/rocket-launchRocketLaunchAnimationProgress story: rockets line up on the pad, then blast off at 100%.
3d-spinner/composite-animationCompositeAnimationSeveral animations as layers (how the prefabs combine effects).

Shapes exported from 3d-spinner/engines/little-3d-engine include cube, tetrahedron, octahedron, pyramid, quad, planeMesh, and several spheres (uvSphere, icosphere, octaSphere, cubeSphere).

The useSpinner hook

Use the hook when you already have a container ref or want to call stop() / setProgress() from code. It returns a stable handle.

import { useRef } from "react";
import { useSpinner } from "3d-spinner-react";
import { starSwarm } from "3d-spinner/prefabs";

function ManualControl() {
  const ref = useRef<HTMLDivElement>(null);
  const spinner = useSpinner(ref, {
    type: "indeterminate",
    animation: () => starSwarm().animation,
  });

  return (
    <>
      <div ref={ref} style={{ width: 240, height: 240 }} />
      <button onClick={() => spinner.stop()}>Stop</button>
    </>
  );
}

The <Spinner> component forwards a ref to the same handle:

const ref = useRef<SpinnerHandle>(null);
<Spinner ref={ref} animation={() => new SpinAnimation()} /* ... */ />;
ref.current?.stop();

API

<Spinner>

A div that hosts the spinner. Takes the options below plus normal div props (className, style, aria-*, ...). Defaults to position: relative and needs a size - with no height, you get a blank 0x0 canvas.

useSpinner(targetRef, config, deps?)

Mounts a spinner into targetRef.current and keeps it in sync. Returns a SpinnerHandle.

Options (SpinnerConfig)

OptionTypeDescription
animationSpinnerAnimation | () => SpinnerAnimationThe visual to play. Prefer a factory. Required.
type"progress" | "indeterminate"Mode. Default "progress".
progressnumberProgress 0..1 (progress mode). Reactive.
timeoutMsnumberAuto-complete after this many ms (progress mode).
untilDateAuto-complete at this time (progress mode).
loop"bounce" | "restart"Loop style (indeterminate mode). Default "bounce".
periodMsnumberMs for one sweep (indeterminate mode). Default 2000.
ariaLabelstringAccessible name of the spinner's progress bar. Default "Loading".
onFinish() => voidRuns when the spinner stops animating on its own: its outro finished, it was stopped before its intro, or it could not start. Not on unmount or rebuild.
depsDependencyListExtra rebuild triggers for values captured in an animation factory. Default [].
showboolean<Spinner> only. false plays the outro, then removes the host div; true again mounts a fresh spinner. Default true.

Structural options (type, loop, periodMs, timeoutMs, until, ariaLabel) rebuild the spinner automatically when they change. progress is applied without a rebuild.

ariaLabel names the progress bar that screen readers announce. An aria-label prop on <Spinner> goes on the host div like any other div attribute.

SpinnerHandle

MethodDescription
setProgress(target)Advance progress toward target (0..1). No-op when indeterminate.
stop()Play the outro, then stop. Keeps the element. Returns a promise that resolves once the outro has finished (at once if already stopped).
destroy()Stop now and remove the element. Called automatically on unmount.

Requirements

React 18+. The spinner draws to a canvas, so it only runs in the browser - not during SSR. It mounts in a client effect after hydration.

The package is marked "use client", so in Next.js App Router you can import <Spinner> without adding your own client wrapper.

Development

npm install
npm run build      # compile src/ to dist/ (ESM + type declarations)
npm run typecheck  # type-check without emitting
npm test           # build, then run the unit tests

The live demo is a separate Vite app in demo/ that runs the source in src/:

cd demo
npm install
npm run dev        # http://localhost:5173/3d-spinner-react/

License

MIT (c) Rune Laang

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Versions 8

Collected releases
VersionPublishedDirect dependenciesOpen version
1.1.0Registry latest30 Sept 20266
1.0.129 Sept 20266
1.0.029 Sept 20266
0.9.531 Jul 20266
0.9.410 Jul 20266
0.9.25 Jul 20266
0.9.12 Jul 20266
0.9.030 Jun 20266

Package advisories

0
No matching advisories collected

This describes our collected data; it is not a security assessment.

Repository

ACCESSIBLE
runelaang/3d-spinner-react

Shared repository data · GitHub

License detected in repository
MIT
License branch / ref
main
Last push
30 Sept 2026
Archived
No
Contributor identities
— · partial collection

Public sponsor counts have not been collected. These signals belong to the repository owner.

Downloads & popularity

0count
Gauge · observed snapshots21 Oct 2026
10.50
2026-10-21: 0 · observed 2026-10-21T15:09:05.9111141Z
21 Oct21 Oct

github · 1 observations shown. Missing days are unknown.

View observed values
Date / source periodValueObserved at
2026-10-2102026-10-21T15:09:05.9111141Z

Metadata history

Meaningful changes
Current collected state Current

Observed since 20 Oct, 19:37 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package:808487dbb9bfd460cb75c25dadbb8833f1991bdde308045575cec3d88b42b352