Sine
React + Tailwind
const MID = 20; // centre line in the 120 x 40 box
// A sine wave as cubic curves, one per half wave. Control points at 0.4244 of
// the half (4 / 3pi) with a height of 4/3 the amplitude match the real curve's
// slope at the crossings and its peak.
function wave(length: number, amp: number, from: number, to: number) {
const half = length / 2;
const k = half * 0.4244;
const h = (amp * 4) / 3;
let d = `M${from} ${MID}`;
let side = -1;
for (let x = from; x < to; x += half) {
const y = (MID + side * h).toFixed(2);
d += ` C${(x + k).toFixed(2)} ${y} ${(x + half - k).toFixed(2)} ${y} ${x + half} ${MID}`;
side = -side;
}
return d;
}
const css = `
.loader-sine { --ink: light-dark(#373a40, #f1f2f3); --echo: light-dark(#6d7179, #7d8087); display: inline-block; font-size: 100px; width: 1.4em; height: 0.4667em; }
.loader-sine svg { display: block; width: 100%; height: 100%; fill: none; stroke-linecap: round; mask-image: linear-gradient(90deg, transparent, #000 24%, #000 76%, transparent); }
.loader-sine path { vector-effect: non-scaling-stroke; }
/* The waves roll one wavelength and start over; the group breathes their height on a slower beat. */
.loader-sine__swell { transform-box: view-box; transform-origin: 60px ${MID}px; animation: loader-sine-swell 3.6s ease-in-out infinite; }
.loader-sine__wave { stroke: var(--ink); stroke-width: 3; animation: loader-sine-flow 1.2s linear infinite; }
.loader-sine__echo { stroke: var(--echo); stroke-width: 2; animation: loader-sine-ebb 2.1s linear infinite; }
@keyframes loader-sine-flow { to { transform: translateX(-40px); } }
@keyframes loader-sine-ebb { from { transform: translateX(-64px); } to { transform: translateX(0); } }
@keyframes loader-sine-swell { 0%, 100% { transform: scaleY(1); } 50% { transform: scaleY(0.42); } }
@media (prefers-reduced-motion: reduce) {
.loader-sine__swell, .loader-sine__wave, .loader-sine__echo { animation: none; }
}
`;
export function Preview() {
return (
<>
<style href="kit-loaders-sine" precedence="kit">
{css}
</style>
<div className="loader-sine" role="status" aria-label="Loading">
<svg viewBox="0 0 120 40" aria-hidden="true">
<g className="loader-sine__swell">
<path className="loader-sine__echo" d={wave(64, 6, -64, 192)} />
<path className="loader-sine__wave" d={wave(40, 11, -40, 200)} />
</g>
</svg>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="loader-sine" role="status" aria-label="Loading">
<svg viewBox="0 0 120 40" aria-hidden="true">
<g class="loader-sine__swell">
<path class="loader-sine__echo" d="M-64 20 C-50.42 12.00 -45.58 12.00 -32 20 C-18.42 28.00 -13.58 28.00 0 20 C13.58 12.00 18.42 12.00 32 20 C45.58 28.00 50.42 28.00 64 20 C77.58 12.00 82.42 12.00 96 20 C109.58 28.00 114.42 28.00 128 20 C141.58 12.00 146.42 12.00 160 20 C173.58 28.00 178.42 28.00 192 20"></path>
<path class="loader-sine__wave" d="M-40 20 C-31.51 5.33 -28.49 5.33 -20 20 C-11.51 34.67 -8.49 34.67 0 20 C8.49 5.33 11.51 5.33 20 20 C28.49 34.67 31.51 34.67 40 20 C48.49 5.33 51.51 5.33 60 20 C68.49 34.67 71.51 34.67 80 20 C88.49 5.33 91.51 5.33 100 20 C108.49 34.67 111.51 34.67 120 20 C128.49 5.33 131.51 5.33 140 20 C148.49 34.67 151.51 34.67 160 20 C168.49 5.33 171.51 5.33 180 20 C188.49 34.67 191.51 34.67 200 20"></path>
</g>
</svg>
</div>CSS
.loader-sine { --ink: light-dark(#373a40, #f1f2f3); --echo: light-dark(#6d7179, #7d8087); display: inline-block; font-size: 100px; width: 1.4em; height: 0.4667em; }
.loader-sine svg { display: block; width: 100%; height: 100%; fill: none; stroke-linecap: round; mask-image: linear-gradient(90deg, transparent, #000 24%, #000 76%, transparent); }
.loader-sine path { vector-effect: non-scaling-stroke; }
/* The waves roll one wavelength and start over; the group breathes their height on a slower beat. */
.loader-sine__swell { transform-box: view-box; transform-origin: 60px 20px; animation: loader-sine-swell 3.6s ease-in-out infinite; }
.loader-sine__wave { stroke: var(--ink); stroke-width: 3; animation: loader-sine-flow 1.2s linear infinite; }
.loader-sine__echo { stroke: var(--echo); stroke-width: 2; animation: loader-sine-ebb 2.1s linear infinite; }
@keyframes loader-sine-flow { to { transform: translateX(-40px); } }
@keyframes loader-sine-ebb { from { transform: translateX(-64px); } to { transform: translateX(0); } }
@keyframes loader-sine-swell { 0%, 100% { transform: scaleY(1); } 50% { transform: scaleY(0.42); } }
@media (prefers-reduced-motion: reduce) {
.loader-sine__swell, .loader-sine__wave, .loader-sine__echo { animation: none; }
}Goal: Create a "Sine" loader.
Visual behavior: Two sine waves flow past each other in opposite directions while their height swells and settles.
Technique:
- Pure CSS: every motion is a keyframe animation, so the plain HTML version needs no script.
- Everything is sized in em from one font-size on the root, so changing that one value scales the whole loader.
- Each wave is drawn as cubic curves, one per half wave, with control points placed so the curve matches a true sine. It slides one wavelength and starts over, so it flows forever.
- A second, fainter wave with a longer wavelength flows the other way.
- Their group scales its height on a slower beat with non-scaling strokes, so the lines keep their weight, and a mask fades both ends.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside it is aria-hidden. Under prefers-reduced-motion both waves rest still.
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 "Sine" loader.
Visual behavior: Two sine waves flow past each other in opposite directions while their height swells and settles.
Technique:
- Pure CSS: every motion is a keyframe animation, so the plain HTML version needs no script.
- Everything is sized in em from one font-size on the root, so changing that one value scales the whole loader.
- Each wave is drawn as cubic curves, one per half wave, with control points placed so the curve matches a true sine. It slides one wavelength and starts over, so it flows forever.
- A second, fainter wave with a longer wavelength flows the other way.
- Their group scales its height on a slower beat with non-scaling strokes, so the lines keep their weight, and a mask fades both ends.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside it is aria-hidden. Under prefers-reduced-motion both waves rest still.
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
Two sine waves flow past each other in opposite directions while their height swells and settles.
- Dependencies
- None
- Difficulty
- Beginner
- Triggers
- continuous, loop, ambient
- 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 it is aria-hidden. Under prefers-reduced-motion both waves rest still.