Atmosphere✨ Premium

Rain

A 2D canvas draws 120 diagonal raindrops via simple pixel addition, with a ripple ring on impact — pure vanilla, zero dependencies.

JSCanvasWeather

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Atmosphere has 57 effects, including 10 free. Explore the category →

3 usage examples

Hero / Landing — Atmospheric-themed launch page — Rain example 1

① Hero / Landing — Atmospheric-themed launch page

WhenLaunching a survival game, a weather app, or a SaaS with a dramatic visual identity — rain anchors the atmosphere before the visitor reads the headline.
WhyThe effect creates an immersive mood with zero network cost: no external asset, dark <code>#0a0a0f</code> background, drops at 30% opacity staying behind a scrim. Text overlay remains the visual priority.
Settings120 drops (default), speed: 6 * Math.random() + 4 → dense cascade. For a light drizzle behind a SaaS headline: reduce to 50 drops (for (let n = 0; n < 50; n++)) and speed: 3 * Math.random() + 2.
Product component — Weather alert or delay notification — Rain example 2

② Product component — Weather alert or delay notification

WhenA delivery, transport, or weather dashboard signals a rain-related disruption — the rain background visually contextualizes the message on the card.
WhyThe effect replaces a stock illustration: self-animated, no network asset, thematically accurate. Drop transparency (30%) keeps the card background readable behind the data.
SettingsChange rgba(150,180,220,0.3) to rgba(150,200,180,0.22) for a green tint (spring rain) or reduce to 60 drops for a lighter backdrop behind dense data.
Conversion micro-interaction — Weather block with CTA redirect — Rain example 3

③ Conversion micro-interaction — Weather block with CTA redirect

WhenA sport or outdoor app informs the user a session is cancelled due to rain — the effect visually confirms the cause and directs attention to the reschedule CTA.
WhyNarrative coherence between the message and the visual makes the interruption feel justified rather than frustrating, increasing the likelihood of clicking the alternative CTA.
SettingsReduce to 50 drops and speed: 4 * Math.random() + 3; add CSS opacity: 0.7 on the .ps-canvas-wrap and a dense scrim so the CTA stays visually dominant.

How it works

The requestAnimationFrame loop runs unconditionally — no IntersectionObserver, no pause mechanism. Each frame, the canvas is fully covered with #0a0a0f (fillRect), erasing the previous frame with no trail. The 120 drops are instantiated at load: each stores {x, y, len, speed} with len ∈ [8,23] px and speed ∈ [4,10] px/frame — fixed per drop, sampled once.

Each drop is rendered as a 1-px stroke from (x, y) to (x + 1, y + len). The constant +1 px/frame horizontal drift sets the diagonal angle: arctan(1/speed), roughly 6–14° depending on the drop's speed. Speed is applied directly (y += speed, x += 1) without delta-time normalization — smoothness is frame-rate dependent. Color: rgba(150,180,220,0.3), blue-grey at 30% opacity.

When y > h, the drop resets (y = -len, random x) and a splash {x, y: h-2, life: 1} is pushed to a second array. Each frame: life -= 0.05 over ~20 frames (~333 ms at 60 fps). The splash renders as an arc of radius 5 × (1 - life): it grows from 0 to 5 px while opacity (0.3 × life) fades out. This is an ephemeral ripple ring — not a persistent ground pool.

The effect implements neither prefers-reduced-motion, nor a ResizeObserver, nor a public API. The canvas is sized once at load via parentElement.getBoundingClientRect(). The IIFE targets getElementById('psRain'): single-instance design, one canvas per page.

Accessibility

  • prefers-reduced-motion not implemented: no window.matchMedia('(prefers-reduced-motion:reduce)') check exists — the animation runs perpetually, even for users sensitive to motion. Add this check manually in your integration.
  • The <canvas> does not carry aria-hidden="true" in the source code — screen readers may announce an empty canvas element. Add this attribute in your integration HTML.
  • The .ps-canvas-wrap container has neither role="img" nor aria-label — the effect is semantically invisible to assistive technology. Add a descriptive label on the parent element.
  • No interactive elements (click, focus): the scene is purely decorative. No intrinsic keyboard navigation issue, but ensure any overlay text remains in the normal tab order.
  • Overlay contrast: the canvas alone (background #0a0a0f, drops at 30% opacity) does not itself constrain text contrast — add a semi-opaque scrim over any text overlay to maintain WCAG AA (ratio ≥ 4.5:1).

Browser compatibility

Requires Canvas 2D Context and requestAnimationFrame — supported in all modern browsers. Zero external dependencies.

Chrome 88+✓ Full
Firefox 87+✓ Full
Safari 15+✓ Full
Edge 88+✓ Full
Mobile iOS✓ Touch OK
Android Chrome✓ Full

Without <code>canvas</code> support, the parent <code>&lt;div&gt;</code> remains visible with its background — no fatal JS errors, overlay text content stays readable.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<div class="demo-preview">
  <div class="ps-canvas-wrap" role="img" aria-label="Animated rain background">
    <canvas class="ps-canvas" id="psRain" aria-hidden="true"></canvas>

    <!-- Optional: scrim + overlay content -->
    <div class="ps-scrim" aria-hidden="true"></div>
    <div class="ps-content">
      <h2 class="ps-title">Your message</h2>
      <button class="ps-cta">CTA</button>
    </div>
  </div>
</div>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Drop count (const 120) 120 Init loop: for (let n = 0; n < 120; n++). 40 = drizzle, 200 = heavy storm. Above 250, performance drops on low-end mobile.
Speed per drop (speed: 6×r+4) 4–10 px/frame Replace 6 * Math.random() + 4: 3 * Math.random() + 2 = slow drizzle, 10 * Math.random() + 8 = violent storm. Not delta-time normalized — frame-rate dependent.
Trail length (len: 15×r+8) 8–23 px Replace 15 * Math.random() + 8: short trails (4 + 4 * Math.random()) = fine rain; long (20 + 20 * Math.random()) = torrential downpour.
Horizontal drift (e.x += 1) 1 px/frame The line e.x += 1 sets the diagonal angle. Increase to 2–3 for strong crosswind; set to 0 for pure vertical fall; negative value for right-to-left rain.
Drop color (rgba) rgba(150,180,220,0.3) Change color and opacity inline in the loop: rgba(220,230,255,0.5) = bright cold rain, rgba(100,130,100,0.3) = stormy green tint, opacity 0.15 = very subtle backdrop.
Splash max radius (5) 0–5 px Modify 5 * (1 - c.life): 8–12 px = visible impact on large screens; 2–3 px = subtle shimmer. Ripple duration controlled by life -= 0.05 (20 frames ≈ 333 ms).

FAQ

The current code includes no IntersectionObserver. To add one: declare a flag let active = true before the IIFE, observe the .ps-canvas-wrap and toggle active on intersection change. In the loop, replace requestAnimationFrame(c) with if (active) requestAnimationFrame(c);.
The IIFE targets getElementById('psRain') — one canvas per page by default. For multiple instances, extract the logic into an initRain(id) function keeping drops and splashes in a closure, then call it for each canvas with a distinct id. The global setupCanvas(id) helper defined outside the IIFE is directly reusable.
No in the current implementation. To add support, insert before the loop starts: if (window.matchMedia('(prefers-reduced-motion:reduce)').matches) return;. You can optionally draw a single static frame first to preserve the visual mood without continuous animation.