Rain on Glass
A slightly frosted pane (3 px blur) in front of a city at dusk: a drop is born at the top every 0.4 to 1.2 s, falls in 3 to 7 s while speeding up, drifts a few pixels and leaves a 40 px trail — five to nine drops at once, measured in all three engines.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Glassmorphism has 47 effects, including 6 free. Explore the category →
3 usage examples



How it works
The .lg-rain-scene scene (260 × 180 px, 14 px corners, a sky graded from night blue #1e3a5f to light blue #6b9fd4) holds a city skyline — nine buildings 10 to 22 px wide, in 60% slate blue — and, in front of it, a .lg-rain-glass pane covering the whole scene with backdrop-filter: blur(3px) (and -webkit-) on layer 1: the city is seen through misted glass. Drops and their trails sit on layer 2, on the pane, hence sharp.
The script starts the rain when 30% of the scene enters the screen (IntersectionObserver, once). It then creates a drop, then the next one 0.4 to 1.2 s later, endlessly. Each drop is a div 4 to 10 px wide and 1.3 times taller, tear-shaped (border-radius: 50%/60% 60% 40% 40%), filled with a white → pale blue → transparent radial gradient with an inner highlight. It starts 10 px above the edge and falls to 10 px below the bottom in 3 to 7 s, on a squared curve — slow at first, fast at the end — drifting sideways by ±15 px at most (sin(π·h)). Its trail (2 px wide, up to 40 px long) follows it and fades as it falls. At the bottom, drop and trail are removed from the page.
Measured every 2 s after start: 3, 6, 7, 9, 8, 6 drops in Chromium 151; 4, 7, 6, 6, 7, 6 in Firefox 153; 3, 6, 5, 6, 6, 7 in WebKit 26.5 — the steady state settles within 3 to 4 s and hovers around six drops. No console error, no dependency. Its only measured weakness is that the rain never stops, even off screen (6 drops still active 3 s after scrolling the scene out of view) — see accessibility and the FAQ to pause it.
Accessibility
- Endless animation: the rain runs as long as the page is open, and nothing is provided for
prefers-reduced-motionor to pause it. An automatic animation lasting more than 5 s must be stoppable (WCAG 2.2.2). The simplest fix: do not start it when the user asked for less motion — in the script, replacea.observe(e)withmatchMedia('(prefers-reduced-motion: reduce)').matches || a.observe(e); the pane and the city remain, dry. - Decoration, not information: the drops tell a screen reader nothing, and must not carry the message alone. Add
aria-hidden="true"on the scene, and give the message as text (“Showers likely 10 am to noon”, “Match postponed”) — in the three scenes on this page, the text sits above or beside the pane, never behind it. - Text on the pane: text placed inside the scene without a
z-indexgoes under the pane and comes out blurred. Put it above (position: absolute; z-index: 3) or outside the scene; over the sky, keep a dark background behind the letters, since the white drops pass over them. - Off screen: the loop keeps running when the scene is no longer visible (measured). No effect for the user, but wasted work on a phone — see the FAQ to stop it out of view.
Browser compatibility
JS: IntersectionObserver, requestAnimationFrame, setTimeout, createElement. CSS: backdrop-filter (and -webkit-), radial gradients, elliptical border-radius. Zero dependencies. Measured in Chromium 151, Firefox 153 and WebKit 26.5: same rhythm and same number of drops within one or two, city blurred by 3 px in all three, no console error.
Without backdrop-filter (Firefox before 103), the city is sharp behind the pane: drops fall normally, only the misted-glass effect disappears. Without IntersectionObserver (Safari before 12.1), the rain never starts: the scene stays a dry city at dusk.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<div class="demo-preview">
<div class="lg-rain-scene" data-rain-scene="">
<div class="lg-rain-glass"></div>
<div class="lg-rain-city">
<div class="lg-rain-building" style="width:12px;height:35px"></div>
…
</div>
<!-- div.lg-raindrop + div.lg-raindrop-trail, added then removed by the script -->
</div>
</div>
The full HTML + CSS + JS of Rain on Glass, ready to paste — plus all 840 effects. Or $299 once, lifetime access.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Drop size (JS — n = 4 + 6 * Math.random(); or CSS — .lg-raindrop { transform: scale(2) }) |
4 to 10 px wide | On a large scene, the original drops are barely visible. Change the formula (8 + 10 * Math.random()) or enlarge them in CSS: the script does not write transform, so a scale applies as is (with transform-origin: 50% 100%). |
Rhythm (JS — setTimeout(t, 400 + 800 * Math.random())) |
one drop every 0.4 to 1.2 s, about six at once | 150 + 300 * Math.random() for a downpour (around fifteen drops), 1500 + 2000 * Math.random() for a sparse drizzle. |
Falling speed (JS — o = 3e3 + 4e3 * Math.random()) |
3 to 7 s to cross the pane, speeding up | 1500 + 1500 * Math.random() for driving rain. The squared curve (h * h) gives the acceleration; plain h would fall at constant speed. |
Drift and trail (JS — l = 30 * (Math.random() - .5), Math.min(40, …)) |
±15 px drift, trail up to 40 px | 0 for drops falling straight, 60 for a wind-lashed window. The maximum trail length is set in Math.min(40, p - r). |
Pane mist (CSS — .lg-rain-glass { backdrop-filter: blur(3px) }) |
3 px | 1 px leaves the city almost sharp, 8 px turns it into patches of light. Add background: rgba(255,255,255,.06) to the pane for milkier glass. |
Landscape behind the pane (HTML — .lg-rain-city and the inline .lg-rain-buildings) |
nine slate-blue buildings against a graded sky | Replace them with any content or a picture (an <img> in position: absolute; inset: 0; object-fit: cover inside the scene, before the pane). Everything under the pane is blurred, everything above stays sharp. |
FAQ
function (x) { v = x[0].isIntersecting; v && !n && (n = !0, t()) }, remove a.disconnect(), declare var v = !1 just before function t(), and at the top of t() add if (!v) { n = !1; return }: the loop stops as soon as the scene leaves the screen and restarts when it comes back (drops in flight finish their fall). Measured with this change: 5 drops in steady state, 0 after 7.5 s out of view, 4 three seconds after coming back on screen — against 6 drops still active out of view with the original code.divs.[data-rain-scene], each gets its own observer and loop, and starts when it reaches the screen. Count about six drops, so a dozen elements, per scene; each drop updates its position every frame, which stays light for two or three scenes.