Pendulum Wave
const css = `
@keyframes loader-pendulum-wave-swing {
from { transform: translateX(-34px); }
to { transform: translateX(34px); }
}
.loader-pendulum-wave {
position: relative;
display: grid;
gap: 4px;
justify-items: center;
width: 96px;
padding: 6px 0;
}
/* Seen from above: each row is one pendulum's track, the bead its bob. */
.loader-pendulum-wave__row {
position: relative;
width: 76px;
height: 4px;
display: grid;
place-items: center;
}
.loader-pendulum-wave__row::before {
content: '';
position: absolute;
left: 0;
right: 0;
top: 50%;
height: 1px;
background: light-dark(#ebedf0, #232529);
}
.loader-pendulum-wave__bob {
position: relative;
width: 6px;
height: 6px;
border-radius: 50%;
background: light-dark(#373a40, #f1f2f3);
box-shadow: 0 0 6px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 50%, transparent);
animation: loader-pendulum-wave-swing var(--half) cubic-bezier(0.37, 0, 0.63, 1) infinite alternate;
animation-delay: calc(var(--half) * -1);
}
@media (prefers-reduced-motion: reduce) {
.loader-pendulum-wave__bob { animation-duration: calc(var(--half) * 4); }
}
`;
// Pendulum i completes 10 + i swings every 12 s, so all twelve line up again every 12 s.
const CYCLE = 12;
const ROWS = Array.from({ length: 12 }, (_, i) => CYCLE / (10 + i) / 2);
export function PendulumWaveLoader() {
return (
<>
<style>
{css}
</style>
<div className="loader-pendulum-wave" role="status" aria-label="Loading">
{ROWS.map((half, i) => (
<span key={i} className="loader-pendulum-wave__row">
<span className="loader-pendulum-wave__bob" style={{ '--half': `${half.toFixed(4)}s` } as React.CSSProperties} />
</span>
))}
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
<div class="loader-pendulum-wave" role="status" aria-label="Loading">
<span class="loader-pendulum-wave__row">
<span class="loader-pendulum-wave__bob" style="--half:0.6000s"></span>
</span>
<span class="loader-pendulum-wave__row">
<span class="loader-pendulum-wave__bob" style="--half:0.5455s"></span>
</span>
<span class="loader-pendulum-wave__row">
<span class="loader-pendulum-wave__bob" style="--half:0.5000s"></span>
</span>
<span class="loader-pendulum-wave__row">
<span class="loader-pendulum-wave__bob" style="--half:0.4615s"></span>
</span>
<span class="loader-pendulum-wave__row">
<span class="loader-pendulum-wave__bob" style="--half:0.4286s"></span>
</span>
<span class="loader-pendulum-wave__row">
<span class="loader-pendulum-wave__bob" style="--half:0.4000s"></span>
</span>
<span class="loader-pendulum-wave__row">
<span class="loader-pendulum-wave__bob" style="--half:0.3750s"></span>
</span>
<span class="loader-pendulum-wave__row">
<span class="loader-pendulum-wave__bob" style="--half:0.3529s"></span>
</span>
<span class="loader-pendulum-wave__row">
<span class="loader-pendulum-wave__bob" style="--half:0.3333s"></span>
</span>
<span class="loader-pendulum-wave__row">
<span class="loader-pendulum-wave__bob" style="--half:0.3158s"></span>
</span>
<span class="loader-pendulum-wave__row">
<span class="loader-pendulum-wave__bob" style="--half:0.3000s"></span>
</span>
<span class="loader-pendulum-wave__row">
<span class="loader-pendulum-wave__bob" style="--half:0.2857s"></span>
</span>
</div>@keyframes loader-pendulum-wave-swing {
from { transform: translateX(-34px); }
to { transform: translateX(34px); }
}
.loader-pendulum-wave {
position: relative;
display: grid;
gap: 4px;
justify-items: center;
width: 96px;
padding: 6px 0;
}
/* Seen from above: each row is one pendulum's track, the bead its bob. */
.loader-pendulum-wave__row {
position: relative;
width: 76px;
height: 4px;
display: grid;
place-items: center;
}
.loader-pendulum-wave__row::before {
content: '';
position: absolute;
left: 0;
right: 0;
top: 50%;
height: 1px;
background: light-dark(#ebedf0, #232529);
}
.loader-pendulum-wave__bob {
position: relative;
width: 6px;
height: 6px;
border-radius: 50%;
background: light-dark(#373a40, #f1f2f3);
box-shadow: 0 0 6px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 50%, transparent);
animation: loader-pendulum-wave-swing var(--half) cubic-bezier(0.37, 0, 0.63, 1) infinite alternate;
animation-delay: calc(var(--half) * -1);
}
@media (prefers-reduced-motion: reduce) {
.loader-pendulum-wave__bob { animation-duration: calc(var(--half) * 4); }
}Goal: Create a "Pendulum Wave" loader.
Visual behavior: A pendulum wave seen from above: twelve bobs swing at slightly different periods, so they fall in and out of step, forming snakes, travelling waves and apparent chaos before lining up in unison again every twelve seconds.
Technique:
- Twelve rows, each a faint track with one glowing bob that swings ±34px on an alternate ease-in-out keyframe.
- Pendulum i completes 10 + i swings every 12s, so its half period (--half) is 12 / (10 + i) / 2 seconds; all twelve realign every 12s.
- A negative delay of one half period starts every bob at the same end, so the pattern begins in unison.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion every period is four times longer, keeping the pattern but slowing it right down.
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 "Pendulum Wave" loader.
Visual behavior: A pendulum wave seen from above: twelve bobs swing at slightly different periods, so they fall in and out of step, forming snakes, travelling waves and apparent chaos before lining up in unison again every twelve seconds.
Technique:
- Twelve rows, each a faint track with one glowing bob that swings ±34px on an alternate ease-in-out keyframe.
- Pendulum i completes 10 + i swings every 12s, so its half period (--half) is 12 / (10 + i) / 2 seconds; all twelve realign every 12s.
- A negative delay of one half period starts every bob at the same end, so the pattern begins in unison.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion every period is four times longer, keeping the pattern but slowing it right down.
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 pendulum wave seen from above: twelve bobs swing at slightly different periods, so they fall in and out of step, forming snakes, travelling waves and apparent chaos before lining up in unison again every twelve seconds.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- continuous, loop, delay stagger
- Techniques
- keyframes, css variables
- 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 every period is four times longer, keeping the pattern but slowing it right down.