Atmosphere✨ Premium

Galaxy Spiral

A 2D canvas animates 400 stars with differential rotation across 3 spiral arms — luminous radial core and persistence via alpha compositing.

JSCanvasGalaxy

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 — Space banner for tech SaaS — Galaxy Spiral example 1

① Hero / Landing — Space banner for tech SaaS

WhenHomepage of a data exploration SaaS, astronomy app, or any product with a 'space' universe — full-width hero section background.
WhyA slowly rotating galaxy creates cinematic depth without overwhelming the text overlay — the alpha compositing trail generates a breathing, dynamic background.
SettingsMax radius reduced to 0.35 (Math.min(t.w,t.h)*0.35) to concentrate the spiral at center; palette narrowed to 3 brand colors (["#a78bfa","#60a5fa","#fff"]); trail alpha at 0.05 for longer trails.
Interactive Planetarium — Main scene of an astronomy app — Galaxy Spiral example 2

② Interactive Planetarium — Main scene of an astronomy app

WhenWeb astronomy application or online planetarium — the galaxy occupies the full main surface, with a minimal overlay UI for navigation and data.
WhyThe 3 spiral arms, differential rotation, and alpha compositing trail are fully legible on a large surface (≥ 400 px) — the effect is the scene itself, not a card accessory.
SettingsMax radius at 0.42 (default) so arms fill all available space; full 5-color palette for a realistic astrophysical look; trail alpha lowered to 0.06 for longer, more cinematic trails (rgba(10,10,15,0.06)).
Conversion micro-interaction — Sign-up confirmation screen — Galaxy Spiral example 3

③ Conversion micro-interaction — Sign-up confirmation screen

WhenThe user has just created an account, completed a payment, or reached their first key milestone — the success screen appears full-screen.
WhyA galaxy background evokes a universe of possibilities opening up — emotional reinforcement of the purchase decision at the most impactful moment in the funnel.
SettingsFull-screen canvas (100 vw/vh); palette narrowed to violet and blue tones (["#a78bfa","#60a5fa","#fff"]) for a premium feel; twist divisor lowered to 50 (dist/50) for tighter, more dramatic arms.

How it works

On load, 400 stars are placed using a spiral formula. Each star belongs to one of 3 arms (index % 3); its initial angle is arm/3 × 2π + dist/80. The dist/80 term is the radial twist: the farther a star is from the center, the more it wraps around its arm. A ±0.5 rad jitter scatters stars naturally around the arm axis. Distance from center is sampled randomly up to Math.min(w,h) × 0.42.

The RAF loop updates angle += speed every frame. Speed decreases with distance: 3×10⁻⁴ + 0.001 × (1 − r/r_max) — this is the differential rotation characteristic of real galaxies (the core rotates faster than the outer arms). The canvas is not cleared between frames: a semi-transparent fillRect with alpha 0.08 progressively covers past positions, producing a persistence trail via alpha compositing — it takes ~12 frames (~200 ms at 60 fps) for a position to fully disappear.

A radial gradient is redrawn every frame over the central core (fixed radius 30 px): it goes from rgba(200,180,255,0.1) to transparent, simulating the glow of a galactic bulge. Each star is an arc() of 0.5–2 px painted in one of 5 hardcoded colors ["#fff","#a78bfa","#ec4899","#fbbf24","#60a5fa"], with individual opacity of 0.3–0.8.

The effect initializes on id="psGalaxy" via getElementByIdone instance per page in the source code. No IntersectionObserver cuts the RAF off-screen (loop runs continuously). No public API is exposed: parameters are JS constants to modify directly in the source.

Accessibility

  • prefers-reduced-motion not handled: the animation runs unconditionally, with no static fallback. At integration time, wrap the init in if (!matchMedia('(prefers-reduced-motion:reduce)').matches) {…} and hide the canvas via @media (prefers-reduced-motion:reduce){.ps-canvas{display:none}}.
  • The <canvas> does not carry aria-hidden="true" in the source — add it so screen readers skip the graphical element.
  • No role="img" or aria-label on the container — supply one at integration time (e.g. aria-label="Animated spiral galaxy — decoration").
  • The effect is purely decorative and non-interactive: no keyboard handlers, no focus targets, no pointer events.
  • Colored stars on an #0a0a0f background provide high visual contrast (decoration only — no text at contrast risk is rendered inside the canvas).

Browser compatibility

Requires Canvas 2D Context and requestAnimationFrame — available in all modern browsers. The CSS uses :has() to style the container (Chrome 105+, Firefox 121+, Safari 15.4+); missing support only affects the wrap's fallback style, not the animation.

Chrome 105+✓ Full
Firefox 121+✓ Full
Safari 15.4+✓ Full
Edge 105+✓ Full
iOS Safari 15.4+✓ Full
Android Chrome✓ Full

If <code>canvas</code> is not supported, the <code>.ps-canvas-wrap</code> renders empty on a dark background. The <code>if (!t) return</code> guard at the top of the IIFE prevents any JS errors.

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">
    <!-- ⚠ id="psGalaxy" is hardcoded in the JS: one instance per page without adaptation -->
    <canvas class="ps-canvas" id="psGalaxy" width="392" height="280"
            aria-hidden="true"></canvas>
  </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
l (color palette) ["#fff","#a78bfa","#ec4899","#fbbf24","#60a5fa"] Star color array (const l=[…] line). 3 colors for a clean look, 7+ for a festive feel. Each color is manually converted to RGB in the draw loop — if you add a color, add its entry to the if/else conversion block too.
400 (star count) 400 Loop size in for(let a=0;a<400;a++). Reduce to 150–200 for a lighter integration on small surfaces or low-power mobile.
a % 3 (spiral arms) 3 The modulo defines the arm count. Change to a%2 for a barred galaxy, a%5 for a very dense spiral. Update the angle formula accordingly: n/3n/N.
80 (radial twist) 80 Divisor in dist/80. Lower to 40 for tightly wound compact arms, raise to 150 for a nearly straight spiral.
0.08 (trail alpha) 0.08 Alpha of the persistence fillRect (rgba(10,10,15,0.08)). Lower to 0.03 for long ghostly trails, raise to 0.25 for a crisp, trail-free render.
0.42 (max radius) 0.42 Factor applied to Math.min(w,h) for the maximum star distance. 0.42 = spiral fits within 84% of the short side. Lower to 0.3 to concentrate, raise to 0.5 to spread to the edges.
30 (core radius) 30 px Radius of the central radial gradient (createRadialGradient(cx,cy,0,cx,cy,30)). Raise to 60–80 for a bright quasar-like core; lower to 10 for a discreet center point.
1.5*Math.random()+.5 (star size) 0.5–2 px Star radius range. Change to 2.5*Math.random()+1 for larger, more visible stars on big screens, or Math.random()+0.3 for tiny, more realistic points.

FAQ

The canvas is not cleared between frames. A fillRect(0,0,w,h) with fillStyle="rgba(10,10,15,0.08)" progressively covers previous positions — the alpha compositing trail technique. At alpha 0.08, a position takes ~12 frames (~200 ms at 60 fps) to fully disappear. Raise this alpha to shorten trails, lower it to lengthen them.
Change const n = a % 3 to const n = a % 4 (or % 5). Update the angle formula: n / 3 * Math.PI * 2 becomes n / 4 * Math.PI * 2. Also raise the twist divisor (e.g. dist/120) to prevent additional arms from overlapping near the center.
Not in its current state — the IIFE launches the RAF without checking prefers-reduced-motion. To fix at integration time: wrap the initialization in if (!window.matchMedia('(prefers-reduced-motion:reduce)').matches) { /* init */ }, and hide the canvas via @media (prefers-reduced-motion:reduce){.ps-canvas{display:none}} in CSS.