Swish UI
All componentsBackgrounds Free

Meteors

Thin streaks fall diagonally across a dark panel at random moments.

It imports two small hooks from @/lib/motion and the colour tokens; the zip includes both.

Demo.tsx

"use client";
import { useEffect, useRef } from "react";
import { useInView, usePrefersReducedMotion } from "@/lib/motion";
import styles from "./demo.module.css";

const POOL = 10;
// Resting spots, used for the first paint and as the static reduced motion view.
const START = Array.from({ length: POOL }, (_, i) => ({ left: `${18 + ((i * 37) % 90)}%`, top: `${((i * 23) % 50) - 10}%` }));

export default function Demo() {
  const root = useRef<HTMLDivElement>(null);
  const meteors = useRef<(HTMLSpanElement | null)[]>([]);
  const inView = useInView(root);
  const reduced = usePrefersReducedMotion();

  // A pool of streaks: each launch grabs an idle one, places it and restarts its keyframes.
  useEffect(() => {
    if (!inView || reduced) return;
    let timer = 0;
    const launch = () => {
      const el = meteors.current.find((m) => m && !m.classList.contains(styles.flying));
      if (el) {
        el.style.left = `${20 + Math.random() * 95}%`;
        el.style.top = `${-15 + Math.random() * 45}%`;
        el.style.animationDuration = `${1.2 + Math.random() * 1.4}s`;
        el.classList.add(styles.flying);
      }
      timer = window.setTimeout(launch, 250 + Math.random() * 700);
    };
    launch();
    return () => clearTimeout(timer);
  }, [inView, reduced]);

  return (
    <div ref={root} className={styles.root}>
      <div className={styles.sky} aria-hidden="true">
        {START.map((pos, i) => (
          <span
            key={i}
            ref={(el) => {
              meteors.current[i] = el;
            }}
            className={styles.meteor}
            style={pos}
            onAnimationEnd={(e) => e.currentTarget.classList.remove(styles.flying)}
          />
        ))}
      </div>
      <div className={styles.copy}>
        <b>Late arrival? Handled overnight.</b>
        <span>Check-in details go out the moment a guest lands.</span>
      </div>
    </div>
  );
}

demo.module.css

.root { container-type: size; position: relative; width: 100%; height: 100%; display: grid; place-items: center; padding: 20px; overflow: hidden; background: var(--ink); color: var(--white); }
.sky { position: absolute; inset: 0; pointer-events: none; }
/* The streak points down and to the left; its bright head is the right end. */
.meteor { position: absolute; width: 110px; height: 1px; opacity: 0; transform-origin: 100% 50%; rotate: 135deg; translate: -110px 0; background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--accent-soft) 70%, transparent) 60%, var(--white)); }
.meteor::after { content: ""; position: absolute; right: -1px; top: -1px; width: 3px; height: 3px; border-radius: 50%; background: var(--white); box-shadow: 0 0 6px 1px var(--accent-soft); }
.flying { animation: fall linear both; }
@keyframes fall {
  0% { opacity: 0; transform: translateX(0); }
  10% { opacity: 1; }
  75% { opacity: 1; }
  100% { opacity: 0; transform: translateX(min(560px, 140cqmax)); }
}

.copy { position: relative; display: grid; gap: 6px; max-width: 24em; text-align: center; }
.copy b { font-size: clamp(18px, 4cqi, 28px); letter-spacing: -0.02em; line-height: 1.15; text-wrap: balance; }
.copy span { font-size: 14px; color: color-mix(in srgb, var(--white) 65%, transparent); }

@media (prefers-reduced-motion: reduce) {
  .meteor { opacity: .35; }
}

Use it for

  • Dark feature panels or night themed sections
  • Small accents around a single message

Avoid

  • Light backgrounds, where the streaks read as scratches
  • Large pages full of other motion

How it works

A fixed pool of ten streak elements is rendered once. A timer with a random interval takes an idle streak, gives it a random start position and duration, and adds a class that runs one keyframe; animationend frees it again. Each streak is rotated 135 degrees and travels along its own X axis, so the gradient head always leads. The timer only runs while the panel is on screen.

Accessibility

The streaks are aria-hidden decoration and the text sits on solid ink for contrast. Reduced motion stops launches and shows a few faint static streaks instead.

Found on