Wiper
React + Tailwind
import type { CSSProperties } from 'react';
const css = `
/* Out across the glass, back home, then a rest while the rain gathers. */
@keyframes loader-wiper-sweep {
0% { transform: rotate(-172deg); animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1); }
34% { transform: rotate(-8deg); }
40% { transform: rotate(-8deg); animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1); }
74%, 100% { transform: rotate(-172deg); }
}
/* Each drop is timed so 0% is the moment the blade reaches it on the way out. */
@keyframes loader-wiper-drop {
0% { opacity: 1; transform: scale(1); }
1.5%, 72% { opacity: 0; transform: scale(0.2); }
76% { opacity: 1; transform: scale(1.25); }
80%, 100% { opacity: 1; transform: scale(1); }
}
.loader-wiper { position: relative; width: 124px; height: 97px; }
.loader-wiper svg { width: 100%; height: 100%; overflow: visible; }
.loader-wiper__glass { fill: light-dark(#ebedf0, #232529); stroke: light-dark(#cbced3, #393c41); stroke-width: 1.2; stroke-linejoin: round; }
.loader-wiper__sheen { fill: none; stroke: light-dark(#dbdde1, #2e3035); stroke-width: 4; stroke-linecap: round; }
.loader-wiper__dash { fill: light-dark(#dbdde1, #2e3035); }
.loader-wiper__drop { transform-box: fill-box; transform-origin: center; animation: loader-wiper-drop 2.6s linear infinite var(--d); }
.loader-wiper__water { fill: light-dark(#5f626a, #a7abb2); opacity: 0.85; }
.loader-wiper__glint { fill: light-dark(#373a40, #f1f2f3); }
.loader-wiper__arm { transform-origin: 50px 66px; animation: loader-wiper-sweep 2.6s infinite; }
.loader-wiper__rod { stroke: light-dark(#5f626a, #a7abb2); stroke-width: 1.8; stroke-linecap: round; }
.loader-wiper__blade { stroke: light-dark(#42454c, #e5e6e9); stroke-width: 2.8; stroke-linecap: round; }
.loader-wiper__pivot { fill: light-dark(#6d7179, #7d8087); stroke: light-dark(#dbdde1, #2e3035); stroke-width: 1; }
@media (prefers-reduced-motion: reduce) {
.loader-wiper__arm, .loader-wiper__drop { animation: none; }
.loader-wiper__arm { transform: rotate(-172deg); }
}
`;
// t: when, as a share of the cycle, the blade passes the drop on its way out.
const DROPS = [
{ x: 16.5, y: 52.4, r: 1.7, t: 0.072 },
{ x: 29.8, y: 41.4, r: 1.9, t: 0.122 },
{ x: 37.3, y: 36.3, r: 2.1, t: 0.143 },
{ x: 53.1, y: 35.4, r: 2, t: 0.177 },
{ x: 64, y: 51, r: 1.7, t: 0.223 },
{ x: 64.7, y: 38.7, r: 1.4, t: 0.203 },
{ x: 78.3, y: 47.9, r: 1.5, t: 0.246 },
{ x: 42.8, y: 48.9, r: 1.7, t: 0.143 },
{ x: 31.6, y: 55.4, r: 1.9, t: 0.089 },
{ x: 46.5, y: 31.3, r: 2, t: 0.163 },
{ x: 70.8, y: 53.9, r: 2, t: 0.25 },
{ x: 54, y: 48.3, r: 2.2, t: 0.184 },
{ x: 61.6, y: 31.7, r: 1.4, t: 0.192 },
{ x: 23.4, y: 54.6, r: 2.3, t: 0.075 },
{ x: 75, y: 38.1, r: 1.5, t: 0.221 },
];
export function WiperLoader() {
return (
<>
<style>
{css}
</style>
<div className="loader-wiper" role="status" aria-label="Loading">
<svg viewBox="0 0 100 78" aria-hidden="true">
<path className="loader-wiper__glass" d="M8 66L16 16Q18 9 25 9H75Q82 9 84 16L92 66Z" />
<path className="loader-wiper__sheen" d="M70 16L58 40M80 22L74 34" />
{DROPS.map((p) => (
<g key={`${p.x}-${p.y}`} className="loader-wiper__drop" style={{ '--d': `${((p.t - 1) * 2.6).toFixed(3)}s` } as CSSProperties}>
<circle className="loader-wiper__water" cx={p.x} cy={p.y} r={p.r} />
<circle className="loader-wiper__glint" cx={p.x - p.r * 0.35} cy={p.y - p.r * 0.35} r={p.r * 0.32} />
</g>
))}
<g className="loader-wiper__arm">
<line className="loader-wiper__rod" x1="50" y1="66" x2="86" y2="66" />
<line className="loader-wiper__blade" x1="62" y1="66" x2="89" y2="66" />
</g>
<rect className="loader-wiper__dash" x="2" y="66" width="96" height="9" rx="3" />
<circle className="loader-wiper__pivot" cx="50" cy="66" r="3.2" />
</svg>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="loader-wiper" role="status" aria-label="Loading">
<svg viewBox="0 0 100 78" aria-hidden="true">
<path class="loader-wiper__glass" d="M8 66L16 16Q18 9 25 9H75Q82 9 84 16L92 66Z"></path>
<path class="loader-wiper__sheen" d="M70 16L58 40M80 22L74 34"></path>
<g class="loader-wiper__drop" style="--d:-2.413s">
<circle class="loader-wiper__water" cx="16.5" cy="52.4" r="1.7"></circle>
<circle class="loader-wiper__glint" cx="15.905" cy="51.805" r="0.544"></circle>
</g>
<g class="loader-wiper__drop" style="--d:-2.283s">
<circle class="loader-wiper__water" cx="29.8" cy="41.4" r="1.9"></circle>
<circle class="loader-wiper__glint" cx="29.135" cy="40.735" r="0.608"></circle>
</g>
<g class="loader-wiper__drop" style="--d:-2.228s">
<circle class="loader-wiper__water" cx="37.3" cy="36.3" r="2.1"></circle>
<circle class="loader-wiper__glint" cx="36.565" cy="35.565" r="0.672"></circle>
</g>
<g class="loader-wiper__drop" style="--d:-2.140s">
<circle class="loader-wiper__water" cx="53.1" cy="35.4" r="2"></circle>
<circle class="loader-wiper__glint" cx="52.4" cy="34.7" r="0.64"></circle>
</g>
<g class="loader-wiper__drop" style="--d:-2.020s">
<circle class="loader-wiper__water" cx="64" cy="51" r="1.7"></circle>
<circle class="loader-wiper__glint" cx="63.405" cy="50.405" r="0.544"></circle>
</g>
<g class="loader-wiper__drop" style="--d:-2.072s">
<circle class="loader-wiper__water" cx="64.7" cy="38.7" r="1.4"></circle>
<circle class="loader-wiper__glint" cx="64.21" cy="38.21" r="0.448"></circle>
</g>
<g class="loader-wiper__drop" style="--d:-1.960s">
<circle class="loader-wiper__water" cx="78.3" cy="47.9" r="1.5"></circle>
<circle class="loader-wiper__glint" cx="77.775" cy="47.375" r="0.48"></circle>
</g>
<g class="loader-wiper__drop" style="--d:-2.228s">
<circle class="loader-wiper__water" cx="42.8" cy="48.9" r="1.7"></circle>
<circle class="loader-wiper__glint" cx="42.205" cy="48.305" r="0.544"></circle>
</g>
<g class="loader-wiper__drop" style="--d:-2.369s">
<circle class="loader-wiper__water" cx="31.6" cy="55.4" r="1.9"></circle>
<circle class="loader-wiper__glint" cx="30.935" cy="54.735" r="0.608"></circle>
</g>
<g class="loader-wiper__drop" style="--d:-2.176s">
<circle class="loader-wiper__water" cx="46.5" cy="31.3" r="2"></circle>
<circle class="loader-wiper__glint" cx="45.8" cy="30.6" r="0.64"></circle>
</g>
<g class="loader-wiper__drop" style="--d:-1.950s">
<circle class="loader-wiper__water" cx="70.8" cy="53.9" r="2"></circle>
<circle class="loader-wiper__glint" cx="70.1" cy="53.2" r="0.64"></circle>
</g>
<g class="loader-wiper__drop" style="--d:-2.122s">
<circle class="loader-wiper__water" cx="54" cy="48.3" r="2.2"></circle>
<circle class="loader-wiper__glint" cx="53.23" cy="47.53" r="0.704"></circle>
</g>
<g class="loader-wiper__drop" style="--d:-2.101s">
<circle class="loader-wiper__water" cx="61.6" cy="31.7" r="1.4"></circle>
<circle class="loader-wiper__glint" cx="61.11" cy="31.21" r="0.448"></circle>
</g>
<g class="loader-wiper__drop" style="--d:-2.405s">
<circle class="loader-wiper__water" cx="23.4" cy="54.6" r="2.3"></circle>
<circle class="loader-wiper__glint" cx="22.595" cy="53.795" r="0.736"></circle>
</g>
<g class="loader-wiper__drop" style="--d:-2.025s">
<circle class="loader-wiper__water" cx="75" cy="38.1" r="1.5"></circle>
<circle class="loader-wiper__glint" cx="74.475" cy="37.575" r="0.48"></circle>
</g>
<g class="loader-wiper__arm">
<line class="loader-wiper__rod" x1="50" y1="66" x2="86" y2="66"></line>
<line class="loader-wiper__blade" x1="62" y1="66" x2="89" y2="66"></line>
</g>
<rect class="loader-wiper__dash" x="2" y="66" width="96" height="9" rx="3"></rect>
<circle class="loader-wiper__pivot" cx="50" cy="66" r="3.2"></circle>
</svg>
</div>CSS
/* Out across the glass, back home, then a rest while the rain gathers. */
@keyframes loader-wiper-sweep {
0% { transform: rotate(-172deg); animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1); }
34% { transform: rotate(-8deg); }
40% { transform: rotate(-8deg); animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1); }
74%, 100% { transform: rotate(-172deg); }
}
/* Each drop is timed so 0% is the moment the blade reaches it on the way out. */
@keyframes loader-wiper-drop {
0% { opacity: 1; transform: scale(1); }
1.5%, 72% { opacity: 0; transform: scale(0.2); }
76% { opacity: 1; transform: scale(1.25); }
80%, 100% { opacity: 1; transform: scale(1); }
}
.loader-wiper { position: relative; width: 124px; height: 97px; }
.loader-wiper svg { width: 100%; height: 100%; overflow: visible; }
.loader-wiper__glass { fill: light-dark(#ebedf0, #232529); stroke: light-dark(#cbced3, #393c41); stroke-width: 1.2; stroke-linejoin: round; }
.loader-wiper__sheen { fill: none; stroke: light-dark(#dbdde1, #2e3035); stroke-width: 4; stroke-linecap: round; }
.loader-wiper__dash { fill: light-dark(#dbdde1, #2e3035); }
.loader-wiper__drop { transform-box: fill-box; transform-origin: center; animation: loader-wiper-drop 2.6s linear infinite var(--d); }
.loader-wiper__water { fill: light-dark(#5f626a, #a7abb2); opacity: 0.85; }
.loader-wiper__glint { fill: light-dark(#373a40, #f1f2f3); }
.loader-wiper__arm { transform-origin: 50px 66px; animation: loader-wiper-sweep 2.6s infinite; }
.loader-wiper__rod { stroke: light-dark(#5f626a, #a7abb2); stroke-width: 1.8; stroke-linecap: round; }
.loader-wiper__blade { stroke: light-dark(#42454c, #e5e6e9); stroke-width: 2.8; stroke-linecap: round; }
.loader-wiper__pivot { fill: light-dark(#6d7179, #7d8087); stroke: light-dark(#dbdde1, #2e3035); stroke-width: 1; }
@media (prefers-reduced-motion: reduce) {
.loader-wiper__arm, .loader-wiper__drop { animation: none; }
.loader-wiper__arm { transform: rotate(-172deg); }
}Goal: Create a "Wiper" loader.
Visual behavior: Rain gathers on a windscreen, the wiper sweeps across and clears every drop it passes, swings home, and the rain starts to gather again.
Technique:
- The wiper sweeps out and back on an ease-in-out curve, then rests. For each drop, the moment the blade reaches it was worked out once from its angle by inverting that curve.
- Every drop runs the same keyframe shifted by its own negative delay, so its 0% is the instant the blade passes: it vanishes there and splashes back in after the wiper has gone home.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the wiper rests and the rain stays.
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 "Wiper" loader.
Visual behavior: Rain gathers on a windscreen, the wiper sweeps across and clears every drop it passes, swings home, and the rain starts to gather again.
Technique:
- The wiper sweeps out and back on an ease-in-out curve, then rests. For each drop, the moment the blade reaches it was worked out once from its angle by inverting that curve.
- Every drop runs the same keyframe shifted by its own negative delay, so its 0% is the instant the blade passes: it vanishes there and splashes back in after the wiper has gone home.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the wiper rests and the rain stays.
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
Rain gathers on a windscreen, the wiper sweeps across and clears every drop it passes, swings home, and the rain starts to gather again.
- Dependencies
- None
- Difficulty
- Advanced
- Triggers
- continuous, loop, delay stagger
- Techniques
- keyframes, css variables, svg
- 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 wiper rests and the rain stays.