Orbit Loader
Original · freeorbiting dots loader
byWeb InnoventixReact + Tailwind
loadorbitloaders
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/load-orbit.jsonload-orbit.tsx
"use client";
import { useEffect, useId, useState } from "react";
import { useReducedMotion } from "motion/react";
type Density = "minimal" | "balanced" | "dense";
const DOTS_PER_RING: Record<Density, number> = {
minimal: 1,
balanced: 2,
dense: 3,
};
const RINGS = [
{
r: 128,
dot: 13,
base: 6.0,
offset: 0,
track: "border-indigo-200/80 dark:border-indigo-400/15",
dotCls:
"bg-indigo-500 dark:bg-indigo-400 shadow-lg shadow-indigo-500/40 dark:shadow-indigo-400/30",
},
{
r: 92,
dot: 11,
base: 4.2,
offset: 40,
track: "border-violet-200/80 dark:border-violet-400/15",
dotCls:
"bg-violet-500 dark:bg-violet-400 shadow-lg shadow-violet-500/40 dark:shadow-violet-400/30",
},
{
r: 58,
dot: 9,
base: 2.8,
offset: 80,
track: "border-sky-200/80 dark:border-sky-400/15",
dotCls:
"bg-sky-500 dark:bg-sky-400 shadow-lg shadow-sky-500/40 dark:shadow-sky-400/30",
},
] as const;
const MESSAGES = [
"Connecting to the sync engine",
"Fetching your latest changes",
"Reconciling offline edits",
"Verifying workspace integrity",
"Warming up the cache",
];
const DENSITY_OPTIONS: Density[] = ["minimal", "balanced", "dense"];
const PlayIcon = (
<svg viewBox="0 0 20 20" className="h-4 w-4" fill="currentColor" aria-hidden="true">
<path d="M6 4.5v11a1 1 0 0 0 1.54.84l8.5-5.5a1 1 0 0 0 0-1.68l-8.5-5.5A1 1 0 0 0 6 4.5Z" />
</svg>
);
const PauseIcon = (
<svg viewBox="0 0 20 20" className="h-4 w-4" fill="currentColor" aria-hidden="true">
<path d="M6 4h3v12H6zM11 4h3v12h-3z" />
</svg>
);
const RestartIcon = (
<svg
viewBox="0 0 20 20"
className="h-4 w-4"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
aria-hidden="true"
>
<path d="M15.6 5.6A6 6 0 1 0 16.9 12" strokeLinecap="round" />
<path d="M16 3.4V6h-2.6" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
export default function LoadOrbit() {
const prefersReduced = useReducedMotion();
const reduced = Boolean(prefersReduced);
const [running, setRunning] = useState(true);
const [speed, setSpeed] = useState(1);
const [density, setDensity] = useState<Density>("balanced");
const [reverse, setReverse] = useState(false);
const [elapsed, setElapsed] = useState(0);
const [msgIndex, setMsgIndex] = useState(0);
const speedId = useId();
const densityName = useId();
useEffect(() => {
if (!running) return;
const id = window.setInterval(() => setElapsed((e) => e + 1), 1000);
return () => window.clearInterval(id);
}, [running]);
useEffect(() => {
if (!running || reduced) return;
const id = window.setInterval(
() => setMsgIndex((i) => (i + 1) % MESSAGES.length),
2800,
);
return () => window.clearInterval(id);
}, [running, reduced]);
const handleRestart = () => {
setElapsed(0);
setMsgIndex(0);
setRunning(true);
};
const playState: "running" | "paused" = running ? "running" : "paused";
const mm = Math.floor(elapsed / 60);
const ss = elapsed % 60;
const time = `${mm}:${String(ss).padStart(2, "0")}`;
const count = DOTS_PER_RING[density];
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 text-slate-900 sm:py-24 dark:bg-slate-950 dark:text-slate-100">
<style>{`
.lo-anim {
animation-name: lo-spin;
animation-timing-function: linear;
animation-iteration-count: infinite;
will-change: transform;
}
.lo-arm {
position: absolute;
top: 0;
left: 50%;
width: 0;
height: 50%;
transform-origin: bottom center;
}
.lo-dot {
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, -50%);
}
.lo-core-anim {
animation: lo-pulse 2s ease-in-out infinite;
}
.lo-bar-anim {
animation-name: lo-bar;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
}
@keyframes lo-spin {
to { transform: rotate(360deg); }
}
@keyframes lo-pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.35); opacity: 0.6; }
}
@keyframes lo-bar {
0% { transform: translateX(-120%); }
100% { transform: translateX(360%); }
}
@media (prefers-reduced-motion: reduce) {
.lo-anim, .lo-core-anim, .lo-bar-anim {
animation: none !important;
}
}
`}</style>
<div className="mx-auto max-w-5xl">
<div className="mb-10 max-w-2xl">
<span className="text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
Loaders · Orbit
</span>
<h2 className="mt-3 text-3xl font-bold tracking-tight sm:text-4xl">
Orbiting dots loader
</h2>
<p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-400">
A lightweight, indeterminate loader built from three counter-rotating
rings. Pause it, change the tempo, or add more dots — every control
below drives the animation in real time.
</p>
</div>
<div className="grid gap-6 lg:grid-cols-2">
{/* Loader panel */}
<div className="flex flex-col items-center justify-center rounded-3xl border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div
aria-hidden="true"
className="relative mx-auto"
style={{ width: 300, height: 300 }}
>
<div className="absolute inset-0 rounded-full bg-[radial-gradient(circle,rgba(99,102,241,0.14),transparent_68%)] dark:bg-[radial-gradient(circle,rgba(129,140,248,0.16),transparent_68%)]" />
{RINGS.map((ring, i) => {
const D = ring.r * 2;
return (
<div
key={`track-${i}`}
className={`absolute rounded-full border ${ring.track}`}
style={{
left: "50%",
top: "50%",
width: D,
height: D,
marginLeft: -ring.r,
marginTop: -ring.r,
}}
/>
);
})}
{RINGS.map((ring, i) => {
const D = ring.r * 2;
const dur = ring.base / speed;
const dir: "normal" | "reverse" =
(i % 2 === 1) !== reverse ? "reverse" : "normal";
return (
<div
key={`ring-${i}`}
className={reduced ? "absolute" : "lo-anim absolute"}
style={{
left: "50%",
top: "50%",
width: D,
height: D,
marginLeft: -ring.r,
marginTop: -ring.r,
animationDuration: `${dur}s`,
animationDirection: dir,
animationPlayState: playState,
}}
>
{Array.from({ length: count }).map((_, k) => {
const angle = k * (360 / count) + ring.offset;
return (
<span
key={k}
className="lo-arm"
style={{ transform: `rotate(${angle}deg)` }}
>
<span
className={`lo-dot rounded-full ${ring.dotCls}`}
style={{ width: ring.dot, height: ring.dot }}
/>
</span>
);
})}
</div>
);
})}
<div
className={`absolute rounded-full bg-emerald-500 shadow-lg shadow-emerald-500/40 dark:bg-emerald-400 dark:shadow-emerald-400/30 ${
reduced ? "" : "lo-core-anim"
}`}
style={{
left: "50%",
top: "50%",
width: 18,
height: 18,
marginLeft: -9,
marginTop: -9,
animationPlayState: playState,
}}
/>
</div>
<span className="sr-only" role="status" aria-live="polite">
{running
? `Loading. ${MESSAGES[msgIndex]}. ${time} elapsed.`
: "Loading paused."}
</span>
{/* Status readout */}
<div className="mt-8 w-full max-w-xs">
<div className="flex items-center justify-between text-sm">
<p
className="font-medium text-slate-700 dark:text-slate-200"
aria-hidden="true"
>
{MESSAGES[msgIndex]}
</p>
<span className="tabular-nums text-slate-400 dark:text-slate-500">
{time}
</span>
</div>
<div className="mt-3 h-1.5 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800">
<div
className={`h-full w-1/3 rounded-full bg-gradient-to-r from-indigo-500 to-violet-500 ${
reduced ? "" : "lo-bar-anim"
}`}
style={{
animationDuration: `${1.6 / speed}s`,
animationPlayState: playState,
}}
/>
</div>
<p className="mt-3 text-xs text-slate-400 dark:text-slate-500">
This usually takes a few seconds.
</p>
</div>
</div>
{/* Controls panel */}
<div className="rounded-3xl border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<h3 className="text-sm font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
Playground
</h3>
<div className="mt-4 flex flex-wrap gap-3">
<button
type="button"
onClick={() => setRunning((r) => !r)}
aria-pressed={running}
className="inline-flex items-center gap-2 rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-slate-900"
>
{running ? PauseIcon : PlayIcon}
{running ? "Pause" : "Play"}
</button>
<button
type="button"
onClick={handleRestart}
className="inline-flex items-center gap-2 rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 transition hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700 dark:focus-visible:ring-offset-slate-900"
>
{RestartIcon}
Restart
</button>
</div>
{/* Speed */}
<div className="mt-8">
<div className="flex items-center justify-between">
<label
htmlFor={speedId}
className="text-sm font-medium text-slate-700 dark:text-slate-200"
>
Speed
</label>
<span className="tabular-nums text-sm text-slate-500 dark:text-slate-400">
{speed.toFixed(2)}×
</span>
</div>
<input
id={speedId}
type="range"
min={0.25}
max={2.5}
step={0.25}
value={speed}
onChange={(e) => setSpeed(Number(e.target.value))}
className="mt-3 w-full cursor-pointer rounded accent-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 dark:accent-indigo-400 dark:focus-visible:ring-offset-slate-900"
/>
</div>
{/* Density */}
<fieldset className="mt-8">
<legend className="text-sm font-medium text-slate-700 dark:text-slate-200">
Dot density
</legend>
<div className="mt-3 grid grid-cols-3 gap-2">
{DENSITY_OPTIONS.map((opt) => (
<label key={opt} className="relative cursor-pointer">
<input
type="radio"
name={densityName}
value={opt}
checked={density === opt}
onChange={() => setDensity(opt)}
className="peer sr-only"
/>
<span className="block rounded-xl border border-slate-300 bg-white px-3 py-2 text-center text-sm font-medium capitalize text-slate-600 transition peer-checked:border-indigo-500 peer-checked:bg-indigo-50 peer-checked:text-indigo-700 peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-2 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:peer-checked:border-indigo-400 dark:peer-checked:bg-indigo-500/15 dark:peer-checked:text-indigo-300 dark:peer-focus-visible:ring-offset-slate-900">
{opt}
</span>
</label>
))}
</div>
</fieldset>
{/* Reverse toggle */}
<label className="mt-8 flex cursor-pointer items-center justify-between gap-4">
<span className="text-sm font-medium text-slate-700 dark:text-slate-200">
Reverse alternate rings
</span>
<span className="relative inline-flex shrink-0">
<input
type="checkbox"
role="switch"
checked={reverse}
onChange={(e) => setReverse(e.target.checked)}
className="peer sr-only"
/>
<span className="h-6 w-11 rounded-full bg-slate-300 transition-colors peer-checked:bg-indigo-500 peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-2 dark:bg-slate-700 dark:peer-checked:bg-indigo-400 dark:peer-focus-visible:ring-offset-slate-900" />
<span className="pointer-events-none absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform peer-checked:translate-x-5" />
</span>
</label>
<p className="mt-8 rounded-xl bg-slate-50 px-4 py-3 text-xs leading-relaxed text-slate-500 dark:bg-slate-800/60 dark:text-slate-400">
Motion respects your system's reduced-motion setting — when enabled,
the rings hold a static diagram and the live announcements stay
quiet.
</p>
</div>
</div>
</div>
</section>
);
}Dependencies
motion
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Spinner Set Loader
Originalset of circular spinners in sizes

Dots Loader
Originalbouncing dots loader

Bars Loader
Originalequalizer bars loader

Ring Loader
Originalring spinner loader

Pulse Loader
Originalpulsing circle loader

Wave Loader
Originalwave loader

Bounce Loader
Originalbouncing ball loader

Gradient Ring Loader
Originalgradient conic ring spinner

Dual Ring Loader
Originaldual counter-rotating rings

Square Loader
Originalmorphing square loader

Typing Loader
Originaltyping indicator loader

Page Overlay Loader
Originalfull-page loading overlay with progress

