OrbitLoader
PreviewCode
// preview
Color from
#8e8e97
Color mid
#dbc2ff
Color to
#ffffff
// install
pnpmnpmyarnbun
npx shadcn@latest add "https://designpass.dev/r/OrbitLoader-TS-TW.json"Install the OrbitLoader component from DesignPass into this project by running:
npx shadcn@latest add "https://designpass.dev/r/OrbitLoader-TS-TW.json"
If the project has no components.json yet, run `npx shadcn@latest init` first.
Then show me a minimal usage example.// source
tsjs
twcss
/*!
* OrbitLoader, a DesignPass.dev component by Ernest Liu (ernestliu.com)
* Docs & live playground: https://designpass.dev/components/orbit-loader
* MIT licensed, keep this notice in copies and adaptations.
*/
"use client";
import React, { useEffect, useRef, type CSSProperties } from "react";
export interface OrbitLoaderProps {
/** Diameter of the loader (px). */
size?: number;
/** Seconds for one revolution of the fastest electron. */
speed?: number;
/** Orbit track color (dim base). */
colorFrom?: string;
/** Electron color at rest (accent). */
colorMid?: string;
/** Nucleus and electron-glow color (bright peak). */
colorTo?: string;
className?: string;
style?: CSSProperties;
}
/**
* An atom: three electrons circling a glowing nucleus on orbital planes tilted
* in real CSS 3D space. Each orbit is a preserve-3d plane rotated to its own
* inclination, carrying a faint track and an electron that sweeps around it,
* swelling as it rounds the near side and shrinking at the far side so the
* inclination reads as true depth. One Web Animations API rotation per orbit
* plus a counter-rotation keeping every electron round, and one nucleus pulse.
* Zero dependencies, honors prefers-reduced-motion.
*/
export default function OrbitLoader({
size = 48,
speed = 1.6,
colorFrom = "#8e8e97",
colorMid = "#dbc2ff",
colorTo = "#ffffff",
className = "",
style,
}: OrbitLoaderProps) {
const carriers = useRef<(HTMLSpanElement | null)[]>([]);
const electrons = useRef<(HTMLSpanElement | null)[]>([]);
const nucleusRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const animations = carriers.current.flatMap((carrier, index) => {
const duration = speed * (1 + index * 0.4) * 1000;
const spin = carrier?.animate(
[{ transform: "rotate(0deg)" }, { transform: "rotate(360deg)" }],
{ duration, iterations: Infinity, easing: "linear" },
);
// Depth cue: the electron peaks in size and brightness on the near half
// of its tilted orbit and dims toward the far half.
const swell = electrons.current[index]?.animate(
[
{ transform: "rotate(0deg) scale(1)", backgroundColor: colorMid, offset: 0 },
{ transform: "rotate(-90deg) scale(1.45)", backgroundColor: colorTo, offset: 0.25 },
{ transform: "rotate(-180deg) scale(1)", backgroundColor: colorMid, offset: 0.5 },
{ transform: "rotate(-270deg) scale(0.6)", backgroundColor: colorFrom, offset: 0.75 },
{ transform: "rotate(-360deg) scale(1)", backgroundColor: colorMid, offset: 1 },
],
{ duration, iterations: Infinity, easing: "linear" },
);
return [spin, swell];
});
const pulse = nucleusRef.current?.animate(
[{ transform: "scale(0.85)" }, { transform: "scale(1.1)" }, { transform: "scale(0.85)" }],
{ duration: speed * 1200, iterations: Infinity, easing: "ease-in-out" },
);
return () => {
animations.forEach((animation) => animation?.cancel());
pulse?.cancel();
};
}, [speed, colorFrom, colorMid, colorTo]);
// Three orbital planes fanned around the vertical, like an atom diagram.
const planes = [
"rotateY(0deg) rotateX(66deg)",
"rotateY(60deg) rotateX(66deg)",
"rotateY(-60deg) rotateX(66deg)",
];
const orbit = size * 0.92;
const electron = size * 0.11;
const nucleus = size * 0.16;
return (
<span
role="status"
aria-label="Loading"
className={`inline-block shrink-0 ${className}`}
style={{ width: size, height: size, perspective: size * 5, ...style }}
>
<span
aria-hidden="true"
className="relative flex h-full w-full items-center justify-center"
style={{ transformStyle: "preserve-3d" }}
>
{planes.map((plane, index) => (
<span
key={index}
className="absolute flex items-center justify-center"
style={{ width: orbit, height: orbit, transform: plane, transformStyle: "preserve-3d" }}
>
<span
className="absolute inset-0 rounded-full"
style={{ border: `1px solid color-mix(in srgb, ${colorFrom} 55%, transparent)` }}
/>
<span
ref={(el) => {
carriers.current[index] = el;
}}
className="absolute inset-0"
style={{ willChange: "transform" }}
>
<span
ref={(el) => {
electrons.current[index] = el;
}}
className="absolute rounded-full"
style={{
width: electron,
height: electron,
left: `calc(50% - ${electron / 2}px)`,
top: -electron / 2,
backgroundColor: colorMid,
boxShadow: `0 0 ${electron}px ${colorMid}`,
willChange: "transform",
}}
/>
</span>
</span>
))}
<span
ref={nucleusRef}
className="absolute rounded-full"
style={{
width: nucleus,
height: nucleus,
backgroundColor: colorTo,
boxShadow: `0 0 ${nucleus * 1.5}px ${colorMid}`,
willChange: "transform",
}}
/>
</span>
</span>
);
}
// props
Need the license details? Read the component license.