Meteor Shower Background

Subtle canvas meteors drifting across a dark sky — a quiet animated background for heroes and landing sections.

Add to your project

Pick the way you like to build.

Let your AI assistant add it

Copy this prompt and paste it into the AI tool building your site.

# Integrate this UI component

Add this component to my project.

1. Install any missing dependencies.
2. Make sure Tailwind CSS is set up and scans the new file.
3. Prefer passing props over editing the component source — changing the component itself may break it.
4. Adjust colors, size, and layout via props or wrapper classes to match my project. You know my codebase better than this snippet does.

## Component source

```tsx
'use client';

import React, { useEffect, useRef, type ReactNode } from 'react';

// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------
//
// children           Content layered above the canvas (headline, CTA, etc.)
// className          Styles for the content wrapper
// containerClassName Styles for the outer shell (size, layout)
// backgroundFill     Solid sky color behind the meteors (hex)
// meteorColor        Streak color (hex)
// meteorCount        How many meteors run at once
// speed              "slow" (default, subtle) or "fast"
//
type MeteorShowerBackgroundProps = {
  children?: ReactNode;
  className?: string;
  containerClassName?: string;
  backgroundFill?: string;
  meteorColor?: string;
  meteorCount?: number;
  speed?: 'slow' | 'fast';
};

// ---------------------------------------------------------------------------
// Meteor state
// ---------------------------------------------------------------------------
type Meteor = {
  x: number;
  y: number;
  len: number;
  speed: number;
  thickness: number;
  life: number;
  maxLife: number;
  angle: number;
  delay: number;
};

// ---------------------------------------------------------------------------
// Spawn a single meteor just off-screen
// ---------------------------------------------------------------------------
function spawnMeteor(width: number, height: number, fast: boolean): Meteor {
  const fromTop = Math.random() > 0.35;
  return {
    x: fromTop ? Math.random() * width * 1.2 : -40,
    y: fromTop ? -40 : Math.random() * height * 0.55,
    len: 100 + Math.random() * 120,
    speed: (fast ? 2.4 : 1.1) + Math.random() * (fast ? 2.2 : 0.9),
    thickness: 0.9 + Math.random() * 1.2,
    life: 0,
    maxLife: 1.2 + Math.random() * 1.4,
    angle: Math.PI / 4 + (Math.random() - 0.5) * 0.18,
    delay: Math.random() * 4,
  };
}

export default function MeteorShowerBackground({
  children,
  className = '',
  containerClassName = '',
  backgroundFill = '#070b16',
  meteorColor = '#e2e8f0',
  meteorCount = 9,
  speed = 'slow',
}: MeteorShowerBackgroundProps) {
  // ---------------------------------------------------------------------------
  // Refs
  // ---------------------------------------------------------------------------
  const containerRef = useRef<HTMLDivElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const rafRef = useRef(0);
  const meteorsRef = useRef<Meteor[]>([]);

  useEffect(() => {
    const canvas = canvasRef.current;
    const container = containerRef.current;
    if (!canvas || !container) return;

    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    const fast = speed === 'fast';
    let width = 0;
    let height = 0;
    let dpr = 1;
    const prefersReducedMotion = window.matchMedia(
      '(prefers-reduced-motion: reduce)'
    ).matches;

    // -------------------------------------------------------------------------
    // Setup helpers
    // -------------------------------------------------------------------------
    const seedMeteors = () => {
      meteorsRef.current = Array.from({ length: meteorCount }, () =>
        spawnMeteor(width, height, fast)
      );
    };

    const resize = () => {
      const rect = container.getBoundingClientRect();
      dpr = Math.min(window.devicePixelRatio || 1, 2);
      width = Math.max(1, Math.floor(rect.width));
      height = Math.max(1, Math.floor(rect.height));
      canvas.width = Math.floor(width * dpr);
      canvas.height = Math.floor(height * dpr);
      canvas.style.width = `${width}px`;
      canvas.style.height = `${height}px`;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      if (!meteorsRef.current.length) seedMeteors();
    };

    // -------------------------------------------------------------------------
    // Draw loop — fill sky, move + fade each meteor, respawn when done
    // -------------------------------------------------------------------------
    const draw = () => {
      ctx.globalAlpha = 1;
      ctx.fillStyle = backgroundFill;
      ctx.fillRect(0, 0, width, height);

      for (let i = 0; i < meteorsRef.current.length; i++) {
        const m = meteorsRef.current[i];

        // Wait before first appearance so meteors don't all start together
        if (!prefersReducedMotion) {
          if (m.delay > 0) {
            m.delay -= fast ? 0.016 : 0.01;
            continue;
          }
          m.x += Math.cos(m.angle) * m.speed;
          m.y += Math.sin(m.angle) * m.speed;
          m.life += fast ? 0.008 : 0.004;
        }

        // Soft fade in / fade out over the meteor's life
        const progress = m.life / m.maxLife;
        const fade =
          progress < 0.2
            ? progress / 0.2
            : progress > 0.65
              ? Math.max(0, 1 - (progress - 0.65) / 0.35)
              : 1;

        const tailX = m.x - Math.cos(m.angle) * m.len;
        const tailY = m.y - Math.sin(m.angle) * m.len;
        const gradient = ctx.createLinearGradient(tailX, tailY, m.x, m.y);
        gradient.addColorStop(0, 'transparent');
        gradient.addColorStop(0.6, `${meteorColor}33`);
        gradient.addColorStop(1, `${meteorColor}aa`);

        ctx.beginPath();
        ctx.strokeStyle = gradient;
        ctx.lineWidth = m.thickness;
        ctx.lineCap = 'round';
        ctx.globalAlpha = fade * 0.55;
        ctx.moveTo(tailX, tailY);
        ctx.lineTo(m.x, m.y);
        ctx.stroke();

        // Respawn once it leaves the view or finishes fading
        const offscreen =
          m.x > width + 80 || m.y > height + 80 || progress >= 1;
        if (offscreen && !prefersReducedMotion) {
          meteorsRef.current[i] = spawnMeteor(width, height, fast);
        }
      }

      ctx.globalAlpha = 1;

      if (!prefersReducedMotion) {
        rafRef.current = requestAnimationFrame(draw);
      }
    };

    // -------------------------------------------------------------------------
    // Start + cleanup
    // -------------------------------------------------------------------------
    resize();
    seedMeteors();
    draw();

    const observer = new ResizeObserver(resize);
    observer.observe(container);

    return () => {
      cancelAnimationFrame(rafRef.current);
      observer.disconnect();
    };
  }, [backgroundFill, meteorColor, meteorCount, speed]);

  // ---------------------------------------------------------------------------
  // Render — canvas behind, content on top
  // ---------------------------------------------------------------------------
  return (
    <div
      ref={containerRef}
      className={[
        'relative flex min-h-[420px] w-full flex-col items-center justify-center overflow-hidden',
        containerClassName,
      ]
        .filter(Boolean)
        .join(' ')}
    >
      <canvas
        ref={canvasRef}
        className='absolute inset-0 z-0 h-full w-full'
        aria-hidden
      />
      <div
        className={['relative z-10 px-6 text-center', className]
          .filter(Boolean)
          .join(' ')}
      >
        {children}
      </div>
    </div>
  );
}

```

Works with your AI tools

ClaudeClaude*
CodexCodex*
LovableLovable
Base44Base44
Figma MakeFigma Make
ReplitReplit*
CursorCursor*
v0v0

* Compatible with most projects that use the default React/Next.js framework.

Built With

ReactTailwind CSSCanvas API

Props

PropTypeDescription
childrenReactNodeContent layered above the canvas (headline, CTA, etc.)
classNamestringStyles for the content wrapper
containerClassNamestringStyles for the outer shell (size, layout)
backgroundFillstringSolid sky color behind the meteors (hex)
meteorColorstringStreak color (hex)
meteorCountnumberHow many meteors run at once
speed'slow' | 'fast'Meteor pace — slow is the subtler default

More Backgrounds Components

Wavy Tide Background preview

Animated canvas waves that fill the container and drift with the pointer — a softer, SaaS-ready take on classic wavy backgrounds.

Staggered neon trails that sweep across a dark field — colors are prop-driven for easy theming.

Soft layered waves in parallax drift — cool indigo tones by default, colors via props.

Aurora Background preview

Northern lights as folding canvas curtains: irregular ribbons and faint rays drifting across a night sky.

A static 3D background grid — a wireframe perspective grid that covers the full container, warped and bowed toward a vanishing point for depth. No animation, no layout shift.