Six loading indicators on one dark sheet: dots orbiting on three rings, an equaliser of bars, a square that morphs into a circle as it walks a path, a stretching ring, a pulse of ripples and a skeleton with shimmering text. Every one is plain CSS keyframes, so nothing needs a script to run. A speed control at the top changes all six at once. The grid is three columns on a desktop and two on a phone.
import { useState } from "react";
const speeds = [
{ label: "Calm", value: 1.6 },
{ label: "Normal", value: 1 },
{ label: "Brisk", value: 0.6 },
];
const loaders = [
{ key: "orbit", name: "Orbit", note: "Three dots, three radii" },
{ key: "bars", name: "Bars", note: "Staggered equaliser" },
{ key: "morph", name: "Morph", note: "Square to circle" },
{ key: "ring", name: "Ring", note: "Stretching arc" },
{ key: "pulse", name: "Pulse", note: "Expanding ripples" },
{ key: "shimmer", name: "Shimmer", note: "Skeleton and text" },
];
const css = `
.six-loaders-root { --sl: 1; }
@keyframes six-loaders-spin { to { transform: rotate(360deg); } }
@keyframes six-loaders-bar { 0%, 100% { transform: scaleY(0.25); opacity: 0.45; } 50% { transform: scaleY(1); opacity: 1; } }
@keyframes six-loaders-morph {
0% { transform: translate(-22px, -22px) rotate(0deg) scale(0.9); border-radius: 10px; }
25% { transform: translate(22px, -22px) rotate(90deg) scale(1.1); border-radius: 50%; }
50% { transform: translate(22px, 22px) rotate(180deg) scale(0.9); border-radius: 10px; }
75% { transform: translate(-22px, 22px) rotate(270deg) scale(1.1); border-radius: 50%; }
100% { transform: translate(-22px, -22px) rotate(360deg) scale(0.9); border-radius: 10px; }
}
@keyframes six-loaders-dash {
0% { stroke-dasharray: 2 150; stroke-dashoffset: 0; }
50% { stroke-dasharray: 96 150; stroke-dashoffset: -28; }
100% { stroke-dasharray: 96 150; stroke-dashoffset: -150; }
}
@keyframes six-loaders-ripple { 0% { transform: scale(0.2); opacity: 0.9; } 100% { transform: scale(1); opacity: 0; } }
@keyframes six-loaders-beat { 0%, 100% { transform: scale(0.85); } 50% { transform: scale(1.15); } }
@keyframes six-loaders-sweep { 0% { background-position: 180% 0; } 100% { background-position: -80% 0; } }
.six-loaders-spin { animation: six-loaders-spin calc(var(--sl) * var(--d, 1.4s)) linear infinite; }
.six-loaders-bar { transform-origin: 50% 100%; animation: six-loaders-bar calc(var(--sl) * 1.1s) ease-in-out infinite; }
.six-loaders-morph { animation: six-loaders-morph calc(var(--sl) * 2.4s) cubic-bezier(0.65, 0, 0.35, 1) infinite; }
.six-loaders-dash { animation: six-loaders-dash calc(var(--sl) * 1.5s) ease-in-out infinite; }
.six-loaders-ripple { animation: six-loaders-ripple calc(var(--sl) * 2.1s) cubic-bezier(0.2, 0.6, 0.35, 1) infinite; }
.six-loaders-beat { animation: six-loaders-beat calc(var(--sl) * 1.05s) ease-in-out infinite; }
.six-loaders-sweep {
background-image: linear-gradient(100deg, rgba(255,255,255,0.1) 30%, rgba(217,249,157,0.6) 50%, rgba(255,255,255,0.1) 70%);
background-size: 260% 100%;
animation: six-loaders-sweep calc(var(--sl) * 1.8s) linear infinite;
}
.six-loaders-text {
background-image: linear-gradient(100deg, #a1a1aa 35%, #ecfccb 50%, #a1a1aa 65%);
background-size: 260% 100%;
-webkit-background-clip: text; background-clip: text; color: transparent;
animation: six-loaders-sweep calc(var(--sl) * 1.8s) linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.six-loaders-root * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; }
.six-loaders-text { color: #d4d4d8; background-image: none; }
}
`;
function Orbit() {
const rings = [
{ size: 84, d: "1.9s", dot: 8, reverse: false, tone: "bg-lime-300" },
{ size: 56, d: "1.3s", dot: 7, reverse: true, tone: "bg-lime-200" },
{ size: 28, d: "0.9s", dot: 6, reverse: false, tone: "bg-white" },
];
return (
<div className="relative h-24 w-24">
{rings.map((r) => (
<div
key={r.size}
className="six-loaders-spin absolute left-1/2 top-1/2 rounded-full border border-white/10"
style={{ width: r.size, height: r.size, marginLeft: -r.size / 2, marginTop: -r.size / 2, ["--d" as string]: r.d, animationDirection: r.reverse ? "reverse" : "normal" }}
>
<span className={`absolute left-1/2 rounded-full ${r.tone}`} style={{ width: r.dot, height: r.dot, top: -r.dot / 2, marginLeft: -r.dot / 2, boxShadow: "0 0 14px rgba(190,242,100,0.9)" }} />
</div>
))}
<span className="absolute left-1/2 top-1/2 h-1.5 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-lime-300/70" />
</div>
);
}
function Bars() {
return (
<div className="flex h-20 items-end gap-2">
{[0, 1, 2, 3, 4, 5, 6].map((i) => (
<span
key={i}
className="six-loaders-bar block h-full w-2 rounded-full bg-gradient-to-t from-lime-500 to-lime-200"
style={{ animationDelay: `calc(var(--sl) * ${-1.1 + Math.abs(3 - i) * 0.13}s)` }}
/>
))}
</div>
);
}
function Morph() {
return (
<div className="relative flex h-24 w-24 items-center justify-center">
<span className="absolute h-[68px] w-[68px] rounded-2xl border border-dashed border-white/15" />
<span className="six-loaders-morph block h-7 w-7 bg-lime-300" style={{ boxShadow: "0 0 24px rgba(190,242,100,0.55)" }} />
</div>
);
}
function Ring() {
return (
<div className="relative h-24 w-24">
<svg viewBox="0 0 60 60" className="six-loaders-spin h-full w-full" style={{ ["--d" as string]: "2s" }} aria-hidden="true">
<circle cx="30" cy="30" r="24" fill="none" stroke="rgba(255,255,255,0.08)" strokeWidth="5" />
<circle cx="30" cy="30" r="24" fill="none" stroke="#bef264" strokeWidth="5" strokeLinecap="round" className="six-loaders-dash" style={{ filter: "drop-shadow(0 0 5px rgba(190,242,100,0.7))" }} />
</svg>
</div>
);
}
function Pulse() {
return (
<div className="relative flex h-24 w-24 items-center justify-center">
{[0, 1, 2].map((i) => (
<span key={i} className="six-loaders-ripple absolute inset-0 rounded-full border-2 border-lime-300" style={{ animationDelay: `calc(var(--sl) * ${-i * 0.7}s)` }} />
))}
<span className="six-loaders-beat block h-5 w-5 rounded-full bg-lime-300" style={{ boxShadow: "0 0 22px rgba(190,242,100,0.8)" }} />
</div>
);
}
function Shimmer() {
return (
<div className="w-full max-w-[220px]">
<div className="flex items-center gap-3">
<span className="six-loaders-sweep h-10 w-10 flex-none rounded-full" />
<div className="flex-1 space-y-2">
<span className="six-loaders-sweep block h-2.5 w-4/5 rounded-full" />
<span className="six-loaders-sweep block h-2.5 w-1/2 rounded-full" />
</div>
</div>
<span className="six-loaders-sweep mt-3 block h-2.5 w-full rounded-full" />
<p className="six-loaders-text mt-3 text-sm font-medium">Loading your workspace</p>
</div>
);
}
const parts: Record<string, () => JSX.Element> = { orbit: Orbit, bars: Bars, morph: Morph, ring: Ring, pulse: Pulse, shimmer: Shimmer };
export default function SixLoadersOrbitBarsMorphRingPulseShimmer() {
const [speed, setSpeed] = useState(1);
return (
<section className="six-loaders-root relative overflow-hidden bg-zinc-950 px-5 py-14 text-white sm:px-8 sm:py-20" style={{ ["--sl" as string]: speed }}>
<style>{css}</style>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 opacity-60"
style={{ backgroundImage: "radial-gradient(60% 50% at 50% 0%, rgba(163,230,53,0.14), transparent 70%), linear-gradient(rgba(255,255,255,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.04) 1px, transparent 1px)", backgroundSize: "100% 100%, 44px 44px, 44px 44px" }}
/>
<div className="relative mx-auto max-w-5xl">
<div className="flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">
<div className="max-w-lg">
<p className="font-mono text-xs uppercase tracking-[0.2em] text-lime-300">Loading states</p>
<h2 className="mt-3 text-3xl font-semibold tracking-tight sm:text-4xl">Six ways to say one moment</h2>
<p className="mt-3 text-zinc-400">Pure CSS, no images and no script. Pick the one that fits the wait and paste it in.</p>
</div>
<div role="group" aria-label="Animation speed" className="inline-flex self-start rounded-full border border-white/10 bg-white/5 p-1 sm:self-auto">
{speeds.map((s) => (
<button
key={s.label}
type="button"
aria-pressed={speed === s.value}
onClick={() => setSpeed(s.value)}
className={`min-h-[40px] rounded-full px-4 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-lime-300 ${speed === s.value ? "bg-lime-300 text-zinc-950" : "text-zinc-300 hover:text-white"}`}
>
{s.label}
</button>
))}
</div>
</div>
<ul className="mt-10 grid grid-cols-2 gap-3 sm:gap-4 lg:grid-cols-3">
{loaders.map((l, i) => {
const Part = parts[l.key];
return (
<li key={l.key} className="group relative flex flex-col overflow-hidden rounded-2xl border border-white/10 bg-zinc-900/70">
<span aria-hidden="true" className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-lime-300/50 to-transparent" />
<div role="status" aria-label={`${l.name} loader`} className="flex h-40 items-center justify-center px-4 sm:h-48">
<Part />
</div>
<div className="flex items-center justify-between gap-2 border-t border-white/10 px-4 py-3">
<div className="min-w-0">
<p className="text-sm font-semibold">{l.name}</p>
<p className="truncate text-xs text-zinc-400">{l.note}</p>
</div>
<span className="font-mono text-xs text-zinc-500">0{i + 1}</span>
</div>
</li>
);
})}
</ul>
</div>
</section>
);
}
Install with one command: npx shadcn@latest add https://we.inc/r/six-loaders-orbit-bars-morph-ring-pulse-shimmer.json
Each one is a few elements with a CSS `@keyframes` animation. The ring animates `stroke-dasharray` on an SVG circle while the whole SVG rotates, the bars scale on the Y axis with staggered negative delays, and the shimmer slides a wide gradient across the element with `background-position`.
The text has a three-stop gradient as its background, `background-clip: text` and a transparent colour, so the gradient shows through the letters. Animating `background-position` moves the bright band from one side to the other.
A negative `animation-delay` starts the animation part way through its cycle. That means the bars and ripples are already spread out on the first frame instead of starting together and drifting apart.
Use the ring or orbit for short waits inside buttons and cards, bars for audio or processing, pulse for something live such as a location or a recording, and the shimmer skeleton when you know the shape of the content that is about to appear.
No. It imports only `useState` from React for the speed control. Remove the control and the component is static markup plus CSS.
Related: Multi-Step Loading Sequence with Drawn Checkmarks, Dismissible Alert Set: Success, Warning, Error and Info, Button Set: Primary, Secondary, Ghost, Icon and Loading.
More loaders or all free UI components. Start for free to have a full site built around it.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.