Coil
React + Tailwind
const css = `
@keyframes loader-coil-stretch {
0%, 100% { transform: scaleY(0.55); }
50% { transform: scaleY(1); }
}
@keyframes loader-coil-bob {
0%, 100% { transform: translateY(-31px); }
50% { transform: translateY(0); }
}
.loader-coil { position: relative; width: 70px; height: 112px; }
.loader-coil__mount {
position: absolute;
left: 15px;
right: 15px;
top: 0;
height: 3px;
border-radius: 2px;
background: light-dark(#6d7179, #7d8087);
}
.loader-coil__spring {
position: absolute;
left: 50%;
top: 3px;
width: 30px;
height: 70px;
margin-left: -15px;
overflow: visible;
transform-origin: top;
animation: loader-coil-stretch 1.3s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
.loader-coil__spring path {
fill: none;
stroke: light-dark(#42454c, #e5e6e9);
stroke-width: 1.5;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
.loader-coil__spring path.loader-coil__back { stroke: light-dark(#cbced3, #393c41); }
.loader-coil__weight {
position: absolute;
left: 50%;
top: 73px;
width: 26px;
height: 22px;
margin-left: -13px;
border-radius: 5px 5px 8px 8px;
background: linear-gradient(light-dark(#50535b, #c6c8cd), light-dark(#6d7179, #7d8087));
box-shadow: inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 50%, transparent);
animation: loader-coil-bob 1.3s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
@media (prefers-reduced-motion: reduce) {
.loader-coil__spring, .loader-coil__weight { animation-duration: 5s; }
}
`;
// A helix seen side-on: front half-turns (bright) and back half-turns (dim), alternating.
const TURNS = 8;
const front: string[] = [];
const back: string[] = [];
for (let i = 0; i < TURNS; i++) {
const y = (i / TURNS) * 70;
const h = 70 / TURNS;
front.push(`M0 ${y.toFixed(2)}C10 ${(y + h * 0.15).toFixed(2)} 30 ${(y + h * 0.35).toFixed(2)} 30 ${(y + h / 2).toFixed(2)}`);
back.push(`M30 ${(y + h / 2).toFixed(2)}C20 ${(y + h * 0.65).toFixed(2)} 0 ${(y + h * 0.85).toFixed(2)} 0 ${(y + h).toFixed(2)}`);
}
export function CoilLoader() {
return (
<>
<style>
{css}
</style>
<div className="loader-coil" role="status" aria-label="Loading">
<span className="loader-coil__mount" aria-hidden="true" />
<svg className="loader-coil__spring" viewBox="0 0 30 70" preserveAspectRatio="none" aria-hidden="true">
<path className="loader-coil__back" d={back.join('')} />
<path d={front.join('')} />
</svg>
<span className="loader-coil__weight" aria-hidden="true" />
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="loader-coil" role="status" aria-label="Loading">
<span class="loader-coil__mount" aria-hidden="true"></span>
<svg class="loader-coil__spring" viewBox="0 0 30 70" preserveAspectRatio="none" aria-hidden="true">
<path class="loader-coil__back" d="M30 4.38C20 5.69 0 7.44 0 8.75M30 13.13C20 14.44 0 16.19 0 17.50M30 21.88C20 23.19 0 24.94 0 26.25M30 30.63C20 31.94 0 33.69 0 35.00M30 39.38C20 40.69 0 42.44 0 43.75M30 48.13C20 49.44 0 51.19 0 52.50M30 56.88C20 58.19 0 59.94 0 61.25M30 65.63C20 66.94 0 68.69 0 70.00"></path>
<path d="M0 0.00C10 1.31 30 3.06 30 4.38M0 8.75C10 10.06 30 11.81 30 13.13M0 17.50C10 18.81 30 20.56 30 21.88M0 26.25C10 27.56 30 29.31 30 30.63M0 35.00C10 36.31 30 38.06 30 39.38M0 43.75C10 45.06 30 46.81 30 48.13M0 52.50C10 53.81 30 55.56 30 56.88M0 61.25C10 62.56 30 64.31 30 65.63"></path>
</svg>
<span class="loader-coil__weight" aria-hidden="true"></span>
</div>CSS
@keyframes loader-coil-stretch {
0%, 100% { transform: scaleY(0.55); }
50% { transform: scaleY(1); }
}
@keyframes loader-coil-bob {
0%, 100% { transform: translateY(-31px); }
50% { transform: translateY(0); }
}
.loader-coil { position: relative; width: 70px; height: 112px; }
.loader-coil__mount {
position: absolute;
left: 15px;
right: 15px;
top: 0;
height: 3px;
border-radius: 2px;
background: light-dark(#6d7179, #7d8087);
}
.loader-coil__spring {
position: absolute;
left: 50%;
top: 3px;
width: 30px;
height: 70px;
margin-left: -15px;
overflow: visible;
transform-origin: top;
animation: loader-coil-stretch 1.3s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
.loader-coil__spring path {
fill: none;
stroke: light-dark(#42454c, #e5e6e9);
stroke-width: 1.5;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
.loader-coil__spring path.loader-coil__back { stroke: light-dark(#cbced3, #393c41); }
.loader-coil__weight {
position: absolute;
left: 50%;
top: 73px;
width: 26px;
height: 22px;
margin-left: -13px;
border-radius: 5px 5px 8px 8px;
background: linear-gradient(light-dark(#50535b, #c6c8cd), light-dark(#6d7179, #7d8087));
box-shadow: inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 50%, transparent);
animation: loader-coil-bob 1.3s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
@media (prefers-reduced-motion: reduce) {
.loader-coil__spring, .loader-coil__weight { animation-duration: 5s; }
}Goal: Create a "Coil" loader.
Visual behavior: A weight bobs on a spring hanging from a bar. The coil stretches open as the weight drops and bunches tight as it rises, with the back half of every turn drawn dimmer so it reads as a helix.
Technique:
- The spring is an SVG helix seen side-on: eight front half-turns (bright) and eight back half-turns (dim) as cubic curves.
- The SVG scales on Y from its top (0.55 to 1) with vector-effect: non-scaling-stroke, so the wire keeps its thickness while the coil stretches.
- The weight translates on the same 1.3s ease-in-out timing, staying on the end of the spring.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the bounce slows to 5s.
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 "Coil" loader.
Visual behavior: A weight bobs on a spring hanging from a bar. The coil stretches open as the weight drops and bunches tight as it rises, with the back half of every turn drawn dimmer so it reads as a helix.
Technique:
- The spring is an SVG helix seen side-on: eight front half-turns (bright) and eight back half-turns (dim) as cubic curves.
- The SVG scales on Y from its top (0.55 to 1) with vector-effect: non-scaling-stroke, so the wire keeps its thickness while the coil stretches.
- The weight translates on the same 1.3s ease-in-out timing, staying on the end of the spring.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the bounce slows to 5s.
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 weight bobs on a spring hanging from a bar. The coil stretches open as the weight drops and bunches tight as it rises, with the back half of every turn drawn dimmer so it reads as a helix.
- Dependencies
- None
- Difficulty
- Beginner
- Triggers
- continuous, loop, spring
- Techniques
- svg, keyframes, transform only
- 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 bounce slows to 5s.