Skip to content

Components / Backgrounds

Meteors

Streaks falling diagonally across a twinkling sky. Each meteor is one element with a bright head, rotated to its angle and sent along its own axis; start, delay, length and pace come from a seeded generator on the server, so the same seed always draws the same sky. Stars are two tiled gradient layers blinking in alternation. Zero JS; fills its positioned parent.

Live demo

Ship tonight.

seed 7 · 0 B

Live, from the library itself: scroll, hover, navigate away and back. The demo is never gated.

Measured

JavaScript of its own

0 B

CSS 5.8 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.

Browser support

Baseline · widely available

Chrome 105 · Firefox 110 · Safari 16; needs container-units.

Install

One command adds the integration, the base tokens and every component. Then import what you use.

npx astro add moonarc
pnpm astro add moonarc
bunx astro add moonarc
src/pages/index.astro
---
import Meteors from '@moonarc/core/Meteors';
---
<section class="relative overflow-hidden bg-black text-white">
  <Meteors count={16} />
  <h1 class="relative">Ship tonight</h1>
</section>
Copy it into your project instead (shadcn registry)

Owns the file, no dependency. The registry item also installs the base tokens.

terminal
npx shadcn@latest add https://moonarc.dev/r/meteors.json

The CLI needs a components.json and the @/* alias, which Setup has. The file lands in src/components/moonarc/.

Source

The whole component. Self-contained styles in a cascade layer so your classes always win; it imports nothing.

Meteors.astro
---
/**
 * Meteors — streaks falling diagonally across a twinkling sky. Each meteor
 * is one element: a line with a bright head, rotated to `angle`, that
 * travels along its own axis and fades; its start, delay, length and pace
 * come from a seeded generator on the server, so the same seed always
 * draws the same sky (the visual test relies on it) and nothing is random
 * at runtime. The stars are two tiled gradient layers that blink in
 * alternation by opacity. Travel is measured in the box's own size
 * (container units), so it crosses any parent. Zero script.
 */
import type { HTMLAttributes } from 'astro/types';

interface Props extends HTMLAttributes<'div'> {
  /** Meteors in flight. */
  count?: number;
  /** Direction of travel in CSS degrees: 0 right, 90 down, 135 down-left, 45 down-right. */
  angle?: number;
  /** Draw the star field. */
  stars?: boolean;
  /** Seconds per crossing (each meteor varies ±40 %). */
  duration?: number;
  /** Meteor colour. */
  color?: string;
  /** Seed for the generator; the same seed is the same sky. */
  seed?: number;
}

const { count = 12, angle = 135, stars = true, duration = 4, color = 'var(--ma-ink)', seed = 3, class: className, style, ...rest } = Astro.props;
const n = Math.max(1, Math.min(40, Math.round(count)));
// mulberry32, the generator GlyphField uses
function mulberry32(a: number) {
  return () => {
    a |= 0;
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
const rand = mulberry32(Math.floor(seed) || 1);
const r2 = (v: number) => Math.round(v * 100) / 100;
const meteors = Array.from({ length: n }, () => ({
  x: r2(rand() * 130 - 15),
  y: r2(rand() * 80 - 40),
  delay: r2(rand()),
  pace: r2(0.6 + rand() * 0.8),
  len: r2(0.5 + rand()),
}));
const inline = [`--ma-meteor-angle:${angle}deg`, `--ma-meteor-dur:${duration}s`, `--ma-meteor-color:${color}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
---

<div class:list={['ma-meteors', className]} aria-hidden="true" data-stars={stars ? '' : undefined} style={inline} {...rest}>
  {meteors.map((m) => <i style={`--ma-meteor-x:${m.x}%;--ma-meteor-y:${m.y}%;--ma-meteor-d:${m.delay};--ma-meteor-p:${m.pace};--ma-meteor-l:${m.len}`}></i>)}
</div>

<style is:global>
  @layer components {
    :where(.ma-meteors) {
      position: absolute;
      inset: 0;
      overflow: hidden;
      pointer-events: none;
      contain: strict;
      container-type: size;
      color: var(--ma-meteor-color, var(--ma-ink));
    }
    /* the sky: two layers of tiled star gradients that blink in alternation */
    :where(.ma-meteors[data-stars])::before,
    :where(.ma-meteors[data-stars])::after {
      content: '';
      position: absolute;
      inset: 0;
      background-image:
        radial-gradient(circle, currentColor 0.6px, transparent 1.1px),
        radial-gradient(circle, currentColor 0.5px, transparent 1px);
      background-size:
        173px 131px,
        97px 211px;
      background-position:
        23px 41px,
        61px 7px;
      opacity: 0.45;
      animation: ma-meteors-twinkle calc(var(--ma-meteor-dur) * 1.5) var(--ma-ease-in-out) infinite alternate;
    }
    :where(.ma-meteors[data-stars])::after {
      background-size:
        149px 167px,
        113px 89px;
      background-position:
        7px 83px,
        91px 29px;
      animation-delay: calc(var(--ma-meteor-dur) * -0.75);
    }
    /* a meteor: a fading line with a bright head at its front, laid along its direction (local x) and sent along its own axis */
    :where(.ma-meteors > i) {
      position: absolute;
      left: var(--ma-meteor-x, 50%);
      top: var(--ma-meteor-y, 0%);
      width: calc(var(--ma-meteor-l, 1) * 20cqmax);
      height: 1px;
      transform-origin: 100% 50%;
      background: linear-gradient(to right, transparent, currentColor);
      opacity: 0;
      will-change: transform, opacity;
      animation: ma-meteor calc(var(--ma-meteor-dur) * var(--ma-meteor-p, 1)) linear infinite;
      animation-delay: calc(var(--ma-meteor-dur) * var(--ma-meteor-d, 0) * -2);
    }
    :where(.ma-meteors > i)::before {
      content: '';
      position: absolute;
      left: 100%;
      top: 50%;
      width: 3px;
      height: 3px;
      translate: -50% -50%;
      border-radius: 50%;
      background: currentColor;
      box-shadow: 0 0 6px 1px currentColor;
    }
    @media (prefers-reduced-motion: reduce) {
      :where(.ma-meteors[data-stars])::before,
      :where(.ma-meteors[data-stars])::after {
        animation: none;
      }
      :where(.ma-meteors > i) {
        display: none;
      }
    }
  }

  @keyframes ma-meteor {
    0% {
      transform: rotate(var(--ma-meteor-angle, 135deg)) translateX(0);
      opacity: 0;
    }
    8% {
      opacity: 1;
    }
    70% {
      opacity: 1;
    }
    100% {
      transform: rotate(var(--ma-meteor-angle, 135deg)) translateX(140cqmax);
      opacity: 0;
    }
  }
  @keyframes ma-meteors-twinkle {
    from {
      opacity: 0.2;
    }
    to {
      opacity: 0.6;
    }
  }
</style>

Props

PropTypeDefaultDescription
countnumber12Meteors in flight, up to 40.
anglenumber135Direction of travel in CSS degrees: 0 right, 90 down, 135 down-left, 45 down-right.
starsbooleantrueDraw the star field.
durationnumber4Seconds per crossing; each meteor varies ±40 % around it.
colorstringvar(--ma-ink)Meteor and star colour.
seednumber3Seed for the generator; the same seed is the same sky.

Reduced motion

The stars stand still; the meteors are not shown.

With ClientRouter

CSS-only; nothing to rebind.

Why it is built this way

Replaces: Meteors (Magic UI) · Meteors (Aceternity) · MeteorShower (Eldora). See the migration table.

Ambient gradient field: three soft colour fields drifting at three coprime speeds, a slow hue rotation so the colours themselves move, and a film-grain layer stepped at 12 fps.

every browseralways

Particles

Backgrounds

A field of drifting dots that part around the cursor, on a canvas that draws only while on screen in a visible tab.

every browseralwayspointer

Retro Grid

Backgrounds

A perspective grid floor sliding toward the viewer.

every browseralways

Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown