Signal
React + Tailwind
const css = `
.loader-signal { display: block; width: 96px; height: 80px; overflow: visible; }
.loader-signal__arc {
fill: none;
stroke: light-dark(#dbdde1, #2e3035);
stroke-width: 6;
stroke-linecap: round;
}
.loader-signal__dot { fill: light-dark(#dbdde1, #2e3035); }
/* The source lights first, then each arc outward; all hold, then go dark together. */
@keyframes loader-signal-dot {
0% { fill: light-dark(#dbdde1, #2e3035); }
10%, 80% { fill: light-dark(#373a40, #f1f2f3); }
90%, 100% { fill: light-dark(#dbdde1, #2e3035); }
}
@keyframes loader-signal-a1 {
0%, 22% { stroke: light-dark(#dbdde1, #2e3035); }
30%, 80% { stroke: light-dark(#373a40, #f1f2f3); }
90%, 100% { stroke: light-dark(#dbdde1, #2e3035); }
}
@keyframes loader-signal-a2 {
0%, 38% { stroke: light-dark(#dbdde1, #2e3035); }
46%, 80% { stroke: light-dark(#373a40, #f1f2f3); }
90%, 100% { stroke: light-dark(#dbdde1, #2e3035); }
}
@keyframes loader-signal-a3 {
0%, 54% { stroke: light-dark(#dbdde1, #2e3035); }
62%, 80% { stroke: light-dark(#373a40, #f1f2f3); }
90%, 100% { stroke: light-dark(#dbdde1, #2e3035); }
}
.loader-signal__arc--1 { animation: loader-signal-a1 2s ease infinite; }
.loader-signal__arc--2 { animation: loader-signal-a2 2s ease infinite; }
.loader-signal__arc--3 { animation: loader-signal-a3 2s ease infinite; }
.loader-signal__dot { animation: loader-signal-dot 2s ease infinite; }
@media (prefers-reduced-motion: reduce) {
.loader-signal__arc, .loader-signal__dot { animation-duration: 6s; }
}
`;
// Concentric arcs about (48, 70), 90 degrees wide, opening upward.
const arc = (r: number) => {
const a = Math.PI / 4;
const x = (s: number) => (48 + s * Math.sin(a) * r).toFixed(2);
const y = (70 - Math.cos(a) * r).toFixed(2);
return `M${x(-1)} ${y}A${r} ${r} 0 0 1 ${x(1)} ${y}`;
};
export function SignalLoader() {
return (
<>
<style>
{css}
</style>
<svg className="loader-signal" viewBox="0 0 96 80" role="status" aria-label="Loading">
<g>
<circle className="loader-signal__dot" cx={48} cy={70} r={5} />
<path className="loader-signal__arc loader-signal__arc--1" d={arc(20)} />
<path className="loader-signal__arc loader-signal__arc--2" d={arc(37)} />
<path className="loader-signal__arc loader-signal__arc--3" d={arc(54)} />
</g>
</svg>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<svg class="loader-signal" viewBox="0 0 96 80" role="status" aria-label="Loading">
<g>
<circle class="loader-signal__dot" cx="48" cy="70" r="5"></circle>
<path class="loader-signal__arc loader-signal__arc--1" d="M33.86 55.86A20 20 0 0 1 62.14 55.86"></path>
<path class="loader-signal__arc loader-signal__arc--2" d="M21.84 43.84A37 37 0 0 1 74.16 43.84"></path>
<path class="loader-signal__arc loader-signal__arc--3" d="M9.82 31.82A54 54 0 0 1 86.18 31.82"></path>
</g>
</svg>CSS
.loader-signal { display: block; width: 96px; height: 80px; overflow: visible; }
.loader-signal__arc {
fill: none;
stroke: light-dark(#dbdde1, #2e3035);
stroke-width: 6;
stroke-linecap: round;
}
.loader-signal__dot { fill: light-dark(#dbdde1, #2e3035); }
/* The source lights first, then each arc outward; all hold, then go dark together. */
@keyframes loader-signal-dot {
0% { fill: light-dark(#dbdde1, #2e3035); }
10%, 80% { fill: light-dark(#373a40, #f1f2f3); }
90%, 100% { fill: light-dark(#dbdde1, #2e3035); }
}
@keyframes loader-signal-a1 {
0%, 22% { stroke: light-dark(#dbdde1, #2e3035); }
30%, 80% { stroke: light-dark(#373a40, #f1f2f3); }
90%, 100% { stroke: light-dark(#dbdde1, #2e3035); }
}
@keyframes loader-signal-a2 {
0%, 38% { stroke: light-dark(#dbdde1, #2e3035); }
46%, 80% { stroke: light-dark(#373a40, #f1f2f3); }
90%, 100% { stroke: light-dark(#dbdde1, #2e3035); }
}
@keyframes loader-signal-a3 {
0%, 54% { stroke: light-dark(#dbdde1, #2e3035); }
62%, 80% { stroke: light-dark(#373a40, #f1f2f3); }
90%, 100% { stroke: light-dark(#dbdde1, #2e3035); }
}
.loader-signal__arc--1 { animation: loader-signal-a1 2s ease infinite; }
.loader-signal__arc--2 { animation: loader-signal-a2 2s ease infinite; }
.loader-signal__arc--3 { animation: loader-signal-a3 2s ease infinite; }
.loader-signal__dot { animation: loader-signal-dot 2s ease infinite; }
@media (prefers-reduced-motion: reduce) {
.loader-signal__arc, .loader-signal__dot { animation-duration: 6s; }
}Goal: Create a "Signal" loader.
Visual behavior: A wireless signal: the source lights first, then each arc outward in turn, all hold, then go dark together and start again.
Technique:
- Three concentric 90deg SVG arcs above a dot, drawn with thick round-capped strokes.
- Each part has its own keyframe on one shared 2s cycle that switches its stroke (or fill) from dim to bright at a later point (10%, 30%, 46%, 62%), then all turn off together at 90%.
- Using separate keyframes rather than delays keeps the "all off together" moment exact.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the cycle slows to 6s.
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 "Signal" loader.
Visual behavior: A wireless signal: the source lights first, then each arc outward in turn, all hold, then go dark together and start again.
Technique:
- Three concentric 90deg SVG arcs above a dot, drawn with thick round-capped strokes.
- Each part has its own keyframe on one shared 2s cycle that switches its stroke (or fill) from dim to bright at a later point (10%, 30%, 46%, 62%), then all turn off together at 90%.
- Using separate keyframes rather than delays keeps the "all off together" moment exact.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the cycle slows to 6s.
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 wireless signal: the source lights first, then each arc outward in turn, all hold, then go dark together and start again.
- Dependencies
- None
- Difficulty
- Beginner
- Triggers
- continuous, loop, delay stagger, icon
- Techniques
- svg, keyframes
- 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 cycle slows to 6s.