Meteors
A subtle diagonal meteor shower behind your content.
Launch something stellar
A subtle meteor shower for hero sections and CTAs.
components/ui/meteors.tsx
"use client";
import { motion } from "motion/react";
import type { ReactNode } from "react";
// Deterministic pseudo-random values so server and client render the same markup.
const seeded = (i: number, salt: number) => {
const x = Math.sin(i * 12.9898 + salt * 78.233) * 43758.5453;
return x - Math.floor(x);
};
export function Meteors({ count = 18, children, className = "" }: { count?: number; children?: ReactNode; className?: string }) {
const meteors = Array.from({ length: count }, (_, i) => ({
left: Math.round(seeded(i, 1) * 120 - 10),
delay: Math.round(seeded(i, 2) * 60) / 10,
duration: 2.5 + Math.round(seeded(i, 3) * 40) / 10,
}));
return (
<div className={`relative isolate overflow-hidden ${className}`}>
{meteors.map((m, i) => (
<motion.span
key={i}
aria-hidden
className="absolute top-0 -z-10 h-px w-24 rotate-[215deg] bg-gradient-to-r from-zinc-500 to-transparent before:absolute before:right-0 before:top-1/2 before:size-1 before:-translate-y-1/2 before:rounded-full before:bg-zinc-500"
style={{ left: `${m.left}%` }}
initial={{ x: 0, y: -40, opacity: 0 }}
animate={{ x: -420, y: 420, opacity: [0, 1, 1, 0] }}
transition={{ duration: m.duration, delay: m.delay, repeat: Infinity, repeatDelay: 1, ease: "linear" }}
/>
))}
{children}
</div>
);
}
export default function Demo() {
return (
<Meteors className="grid h-72 w-full max-w-2xl place-items-center rounded-3xl border border-zinc-200 bg-white">
<div className="text-center">
<h3 className="text-3xl font-semibold tracking-tight text-zinc-900">Launch something stellar</h3>
<p className="mt-2 text-zinc-500">A subtle meteor shower for hero sections and CTAs.</p>
</div>
</Meteors>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/meteors.tsxcomponents/ui/meteors.tsx"use client"; import { motion } from "motion/react"; import type { ReactNode } from "react"; // Deterministic pseudo-random values so server and client render the same markup. const seeded = (i: number, salt: number) => { const x = Math.sin(i * 12.9898 + salt * 78.233) * 43758.5453; return x - Math.floor(x); }; export function Meteors({ count = 18, children, className = "" }: { count?: number; children?: ReactNode; className?: string }) { const meteors = Array.from({ length: count }, (_, i) => ({ left: Math.round(seeded(i, 1) * 120 - 10), delay: Math.round(seeded(i, 2) * 60) / 10, duration: 2.5 + Math.round(seeded(i, 3) * 40) / 10, })); return ( <div className={`relative isolate overflow-hidden ${className}`}> {meteors.map((m, i) => ( <motion.span key={i} aria-hidden className="absolute top-0 -z-10 h-px w-24 rotate-[215deg] bg-gradient-to-r from-zinc-500 to-transparent before:absolute before:right-0 before:top-1/2 before:size-1 before:-translate-y-1/2 before:rounded-full before:bg-zinc-500" style={{ left: `${m.left}%` }} initial={{ x: 0, y: -40, opacity: 0 }} animate={{ x: -420, y: 420, opacity: [0, 1, 1, 0] }} transition={{ duration: m.duration, delay: m.delay, repeat: Infinity, repeatDelay: 1, ease: "linear" }} /> ))} {children} </div> ); } export default function Demo() { return ( <Meteors className="grid h-72 w-full max-w-2xl place-items-center rounded-3xl border border-zinc-200 bg-white"> <div className="text-center"> <h3 className="text-3xl font-semibold tracking-tight text-zinc-900">Launch something stellar</h3> <p className="mt-2 text-zinc-500">A subtle meteor shower for hero sections and CTAs.</p> </div> </Meteors> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.