Sound Ring
React + Tailwind
import { useMemo } from 'react';
const css = `
@keyframes loader-sound-ring-pump {
0%, 100% { transform: scaleY(0.25); }
50% { transform: scaleY(1); }
}
@keyframes loader-sound-ring-turn { to { transform: rotate(360deg); } }
.loader-sound-ring {
position: relative;
width: 112px;
height: 112px;
animation: loader-sound-ring-turn 12s linear infinite;
}
.loader-sound-ring__bar {
position: absolute;
left: 50%;
top: 50%;
width: 3px;
height: 0;
}
.loader-sound-ring__bar::before {
content: '';
position: absolute;
left: 0;
bottom: 26px;
width: 3px;
height: 24px;
border-radius: 2px;
background: linear-gradient(light-dark(#373a40, #f1f2f3), light-dark(#6d7179, #7d8087));
transform-origin: 50% 100%;
animation: loader-sound-ring-pump var(--t) cubic-bezier(0.45, 0, 0.55, 1) var(--d) infinite;
}
.loader-sound-ring__hub {
position: absolute;
left: 50%;
top: 50%;
width: 36px;
height: 36px;
margin: -18px;
border-radius: 50%;
border: 1px solid light-dark(#cbced3, #393c41);
background: radial-gradient(circle, light-dark(#ebedf0, #232529), light-dark(#fbfcfd, #121417));
}
@media (prefers-reduced-motion: reduce) {
.loader-sound-ring { animation: none; }
.loader-sound-ring__bar::before { animation: none; transform: scaleY(0.5); }
}
`;
const BARS = 40;
export function SoundRingLoader() {
// Neighbouring bars share a "band" (a beat length) so the ring pumps in clusters, like a spectrum.
const bars = useMemo(() => {
let seed = 13;
const rand = () => ((seed = (seed * 16807) % 2147483647) / 2147483647);
return Array.from({ length: BARS }, (_, i) => {
const band = Math.floor(i / 5);
return {
transform: `rotate(${(i * 360) / BARS}deg)`,
'--t': `${(0.5 + ((band * 7) % 5) * 0.12).toFixed(2)}s`,
'--d': `${(-rand() * 0.6 - (i % 5) * 0.05).toFixed(2)}s`,
} as React.CSSProperties;
});
}, []);
return (
<>
<style>
{css}
</style>
<div className="loader-sound-ring" role="status" aria-label="Loading">
{bars.map((s, i) => (
<span key={i} className="loader-sound-ring__bar" style={s} aria-hidden="true" />
))}
<span className="loader-sound-ring__hub" aria-hidden="true" />
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="loader-sound-ring" role="status" aria-label="Loading">
<span class="loader-sound-ring__bar" style="transform:rotate(0deg);--t:0.50s;--d:-0.00s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(9deg);--t:0.50s;--d:-0.48s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(18deg);--t:0.50s;--d:-0.59s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(27deg);--t:0.50s;--d:-0.73s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(36deg);--t:0.50s;--d:-0.76s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(45deg);--t:0.74s;--d:-0.51s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(54deg);--t:0.74s;--d:-0.42s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(63deg);--t:0.74s;--d:-0.60s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(72deg);--t:0.74s;--d:-0.65s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(81deg);--t:0.74s;--d:-0.29s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(90deg);--t:0.98s;--d:-0.59s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(99deg);--t:0.98s;--d:-0.50s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(108deg);--t:0.98s;--d:-0.58s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(117deg);--t:0.98s;--d:-0.42s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(126deg);--t:0.98s;--d:-0.62s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(135deg);--t:0.62s;--d:-0.53s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(144deg);--t:0.62s;--d:-0.48s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(153deg);--t:0.62s;--d:-0.16s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(162deg);--t:0.62s;--d:-0.74s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(171deg);--t:0.62s;--d:-0.72s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(180deg);--t:0.86s;--d:-0.26s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(189deg);--t:0.86s;--d:-0.61s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(198deg);--t:0.86s;--d:-0.49s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(207deg);--t:0.86s;--d:-0.21s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(216deg);--t:0.86s;--d:-0.20s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(225deg);--t:0.50s;--d:-0.51s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(234deg);--t:0.50s;--d:-0.17s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(243deg);--t:0.50s;--d:-0.40s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(252deg);--t:0.50s;--d:-0.39s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(261deg);--t:0.50s;--d:-0.27s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(270deg);--t:0.74s;--d:-0.50s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(279deg);--t:0.74s;--d:-0.60s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(288deg);--t:0.74s;--d:-0.35s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(297deg);--t:0.74s;--d:-0.52s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(306deg);--t:0.74s;--d:-0.54s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(315deg);--t:0.98s;--d:-0.16s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(324deg);--t:0.98s;--d:-0.18s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(333deg);--t:0.98s;--d:-0.60s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(342deg);--t:0.98s;--d:-0.68s" aria-hidden="true"></span>
<span class="loader-sound-ring__bar" style="transform:rotate(351deg);--t:0.98s;--d:-0.65s" aria-hidden="true"></span>
<span class="loader-sound-ring__hub" aria-hidden="true"></span>
</div>CSS
@keyframes loader-sound-ring-pump {
0%, 100% { transform: scaleY(0.25); }
50% { transform: scaleY(1); }
}
@keyframes loader-sound-ring-turn { to { transform: rotate(360deg); } }
.loader-sound-ring {
position: relative;
width: 112px;
height: 112px;
animation: loader-sound-ring-turn 12s linear infinite;
}
.loader-sound-ring__bar {
position: absolute;
left: 50%;
top: 50%;
width: 3px;
height: 0;
}
.loader-sound-ring__bar::before {
content: '';
position: absolute;
left: 0;
bottom: 26px;
width: 3px;
height: 24px;
border-radius: 2px;
background: linear-gradient(light-dark(#373a40, #f1f2f3), light-dark(#6d7179, #7d8087));
transform-origin: 50% 100%;
animation: loader-sound-ring-pump var(--t) cubic-bezier(0.45, 0, 0.55, 1) var(--d) infinite;
}
.loader-sound-ring__hub {
position: absolute;
left: 50%;
top: 50%;
width: 36px;
height: 36px;
margin: -18px;
border-radius: 50%;
border: 1px solid light-dark(#cbced3, #393c41);
background: radial-gradient(circle, light-dark(#ebedf0, #232529), light-dark(#fbfcfd, #121417));
}
@media (prefers-reduced-motion: reduce) {
.loader-sound-ring { animation: none; }
.loader-sound-ring__bar::before { animation: none; transform: scaleY(0.5); }
}Goal: Create a "Sound Ring" loader.
Visual behavior: A circular spectrum: forty bars round a ring pump to their own beats in clusters, like frequency bands, while the whole ring turns slowly.
Technique:
- Forty zero-height bars sit at the centre, each rotated i x 9deg; the visible bar is a ::before pushed outward and scaled on Y from its inner end.
- Groups of five neighbours share a beat length, so the ring pumps in clusters like a spectrum; seeded negative delays offset each bar within its band.
- The ring rotates once every 12s round a dark hub.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the bars hold at half height and the ring stops turning.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Sound Ring" loader.
Visual behavior: A circular spectrum: forty bars round a ring pump to their own beats in clusters, like frequency bands, while the whole ring turns slowly.
Technique:
- Forty zero-height bars sit at the centre, each rotated i x 9deg; the visible bar is a ::before pushed outward and scaled on Y from its inner end.
- Groups of five neighbours share a beat length, so the ring pumps in clusters like a spectrum; seeded negative delays offset each bar within its band.
- The ring rotates once every 12s round a dark hub.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the bars hold at half height and the ring stops turning.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A circular spectrum: forty bars round a ring pump to their own beats in clusters, like frequency bands, while the whole ring turns slowly.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- continuous, loop, delay stagger
- Techniques
- keyframes, css variables, pseudo elements
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the bars hold at half height and the ring stops turning.