A draggable 3D loading animation. The logo is extruded into a thin slab, then several slabs are stacked a small distance apart and spun at whole-revolution multiples of a shared cycle, so they drift apart and snap back into perfect alignment once per cycle. Rear slabs fade toward your background colour for depth.
No dependencies beyond React. Single component, fully prop-driven. Ships TypeScript source - no separate @types package needed.
https://innerworks-me.github.io/innerworks-loader/
npm run dev # build + watch + dev server at http://localhost:8080
npm run build # one-off build of demo/demo.jsnpm install @innerworks-me/innerworks-loaderimport InnerworksLoader from "@innerworks-me/innerworks-loader";
export default function Loading() {
return (
<InnerworksLoader
size={240}
logoColor="#111111"
fadeColor="#ffffff"
speed={1.2}
realignRevs={6}
draggable
onAlign={() => console.log("aligned!")}
/>
);
}onAlign fires every time the slabs line up — once per realign cycle. It's wired
to the actual animation (the rear slab completes exactly one revolution per cycle),
so it stays in sync regardless of speed or tab throttling. Handy for pulsing a
glow, advancing a step, or triggering a sound on the "click" moment.
const [pulse, setPulse] = useState(false);
<InnerworksLoader
onAlign={() => {
setPulse(true);
setTimeout(() => setPulse(false), 150);
}}
style={{ filter: pulse ? "brightness(1.4)" : "none", transition: "filter .15s" }}
/><InnerworksLoader draggable={false} initialRotation={{ x: 0, y: -24 }} /><InnerworksLoader tiltAxis="y" /> // left/right only| Prop | Type | Default | Description |
|---|---|---|---|
size |
number |
300 |
Square render size in px. All geometry scales with it. |
logoColor |
string |
"#111111" |
Logo fill colour. |
fadeColor |
string |
"#ffffff" |
Colour the rear slabs fade toward (set to your background). |
fadeMin |
number |
32 |
% of logoColor used by the rear-most slab. Lower = fainter. |
slabs |
number |
5 |
Number of stacked slabs. |
sheets |
number |
14 |
Copies stacked per slab to fake the extruded thickness. |
thicknessMm |
number |
2 |
Extrusion depth per slab, in mm. |
spacingMm |
number |
3 |
Gap between slabs, in mm. |
pxPerMm |
number |
7 |
Pixels per mm at size=300. |
perspective |
number |
1500 |
CSS perspective at size=300, in px. |
speed |
number |
1 |
Speed multiplier. |
realignRevs |
number |
5 |
Revolutions the fastest slab makes before everything realigns. |
cycleSeconds |
number |
14 |
Seconds for one full realign cycle at speed=1. |
draggable |
boolean |
true |
Allow click/touch drag (and arrow keys) to rotate. |
dragSensitivity |
number |
0.55 |
Degrees of rotation per pixel dragged. |
initialRotation |
{ x: number; y: number } |
{ x: 0, y: -20 } |
Starting angle; the fixed angle when draggable is false. |
tiltAxis |
"both" | "x" | "y" |
"both" |
Restrict drag/keys to one axis. |
paused |
boolean |
false |
Freeze the spin (keeps current phase). |
respectReducedMotion |
boolean |
true |
Freeze when the user prefers reduced motion. |
fireAlignOnMount |
boolean |
false |
Also fire onAlign once for the initial aligned state. |
onAlign |
() => void |
— | Called every time the slabs line up. |
Any other props (className, style, id, data attributes, …) are spread onto
the root element.
- Realign math: each slab does a distinct whole number of revolutions per cycle (rear = 1, front =
realignRevs), so they only all coincide at the cycle boundary. That's the single alignment momentonAlignreports. - Theming: the depth fade blends
logoColortowardfadeColorwith CSScolor-mix. SetfadeColorto match wherever you mount it so the rear slabs recede correctly. For a transparent backdrop, setfadeColorto your page colour rather thantransparent. - Accessibility: when draggable, the root is a focusable
sliderand responds to arrow keys (hold Shift for larger steps). Reduced-motion users see a static, aligned logo by default.