Aurora Background
BackgroundsAmbient 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.
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.
Ship tonight.
seed 7 · 0 B
Live, from the library itself: scroll, hover, navigate away and back. The demo is never gated.
Adjust · props are custom properties, set inline, so you edit the running instance
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
Chrome 105 · Firefox 110 · Safari 16; needs container-units.
One command adds the integration, the base tokens and every component. Then import what you use.
npx astro add moonarcpnpm astro add moonarcbunx astro add moonarc---
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>Owns the file, no dependency. The registry item also installs the base tokens.
npx shadcn@latest add https://moonarc.dev/r/meteors.jsonThe CLI needs a components.json and the @/* alias, which Setup has. The file lands in src/components/moonarc/.
The whole component. Self-contained styles in a cascade layer so your classes always win; it imports nothing.
---
/**
* 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>| Prop | Type | Default | Description |
|---|---|---|---|
count | number | 12 | Meteors in flight, up to 40. |
angle | number | 135 | Direction of travel in CSS degrees: 0 right, 90 down, 135 down-left, 45 down-right. |
stars | boolean | true | Draw the star field. |
duration | number | 4 | Seconds per crossing; each meteor varies ±40 % around it. |
color | string | var(--ma-ink) | Meteor and star colour. |
seed | number | 3 | Seed for the generator; the same seed is the same sky. |
The stars stand still; the meteors are not shown.
ClientRouterCSS-only; nothing to rebind.
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.
A field of drifting dots that part around the cursor, on a canvas that draws only while on screen in a visible tab.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown