Backgrounds

Meteors

A decorative diagonal shooting-star field for hero sections and card backdrops — pure CSS, deterministically scattered.

Preview in your theme

Loading preview…

import * as React from "react"
import { cn } from "@/lib/utils"

/**
 * The transform stays rotate(135deg) for the whole cycle — a 45deg diagonal
 * line — while translateX walks the bar along that same rotated axis, so it
 * travels top-right -> bottom-left with the gradient's bright tip leading.
 * Opacity fades it in a few percent into the cycle and out before the end.
 */
const KEYFRAMES = `@keyframes zy-meteor{0%{transform:rotate(135deg) translateX(0);opacity:0}8%,70%{opacity:1}100%{transform:rotate(135deg) translateX(560px);opacity:0}}`

/**
 * Deterministic hash for (index, salt) -> [0, 1). Render must stay pure
 * (no Math.random()): every meteor's start position, phase delay and

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/meteors.json

Prompt

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

Build a React + TypeScript + Tailwind "Meteors" component — a decorative
diagonal shooting-star field for hero sections and card backdrops. Its only
dependency is a cn() class merger (clsx + tailwind-merge). No hooks, no
events, no browser APIs, so it must stay a server component — do NOT add
"use client".

Contract
- export function Meteors(props): props extend React.ComponentProps<"div">
  (rest props spread onto the root div) plus:
  - count?: number (default 12) — how many meteor streaks to render.
- children render above the effect layer; className merges onto the root via
  cn(), so sizing, borders, rounding and the background color come from the
  call site.

Behavior
- Root div: relative isolate overflow-hidden. An absolutely-positioned effect
  layer (aria-hidden, pointer-events-none, inset-0) holds `count` meteor
  spans; children sit in a separate relative z-10 wrapper above it.
- Each meteor is a thin bar (h-px w-12) animated via one shared @keyframes
  loop: the transform stays rotate(135deg) for the whole cycle (a 45deg
  diagonal line) while translateX walks it along that same rotated axis, so
  it travels top-right -> bottom-left; opacity fades in a few percent into
  the cycle and fades out before the end, so it never pops in or vanishes
  abruptly at the seams.
- Per-meteor start position (top/left %), phase delay and duration are NOT
  Math.random() — render must stay a pure function of props (React purity
  rule + stable SSR output). Instead a small integer hash, e.g.
  Math.imul(i * 374761393 + salt * 668265263 + 0x9e3779b9, 2654435761) >>> 0
  divided by 2**32, maps (index, salt) to a deterministic [0, 1) value. Four
  salted calls per index derive top%, left%, a duration mapped into a
  1.6s-3.2s band, and a NEGATIVE delay (-hash * duration), so the field looks
  fully scattered and mid-flight on first paint instead of every meteor
  launching from the same point in sync.
- The @keyframes ship inside the component via a React 19 hoisted
  <style href="zyeon-meteors" precedence="medium"> tag — no Tailwind config
  edits, and multiple instances dedupe to one style tag by href.
- prefers-reduced-motion: every meteor span gets motion-reduce:hidden — this
  is pure decoration, so under reduced motion it disappears entirely rather
  than freezing mid-animation; children are unaffected either way.

Rendering & styling
- Semantic tokens only. Each meteor is
  bg-gradient-to-r from-transparent via-foreground/40 to-foreground/70 — the
  bright tip (to-foreground/70) leads the direction of travel, the tail fades
  to transparent behind it. No hex / rgb() / oklch() anywhere, so the streaks
  re-skin themselves with the host theme and dark mode automatically.
- Merge consumer className via cn() on the root div; the component sets no
  background of its own, so a light card, an inverted bg-foreground hero, or
  any other surface all work as the backdrop.

Customization levers
- Count: the count prop is the density knob — a handful (4-8) reads as a
  restrained accent on a card, 20+ reads as a dense shower for a hero or
  launch screen.
- Speed range: the 1.6s-3.2s duration band lives in one expression in the
  hash-to-duration mapping; widen or shift it to speed up or slow down the
  whole field uniformly.
- Angle: the shared rotate(135deg) is the only place the flight angle is set;
  swap it (and the translateX sign if needed) to fly meteors at a different
  diagonal.
- Color: swap via-foreground/to-foreground for via-primary/to-primary (or
  any other token) for tinted, on-brand meteors instead of neutral ones.
- Region clipping: cap the hash-derived top% range (e.g. 0-40 instead of
  0-100) to keep meteors confined to the upper portion of a tall container
  instead of the full height.

Concepts

  • Deterministic pseudo-randomness — a seeded integer hash replaces Math.random(), so every meteor's position, delay and duration are a pure function of its index: identical output on the server and the client, no hydration mismatch, no reshuffling on re-render.
  • Phase-staggered delay — each meteor's animation-delay is a negative multiple of its own duration, so the field starts mid-cycle and scattered rather than every streak launching from the same point in lockstep.
  • Single rotated axis — the flight keyframes hold rotate(135deg) fixed and only animate translateX, so the bar always travels along its own long axis — a 45° diagonal — with the gradient's bright end naturally leading.
  • Token-derived streak — the bar's gradient runs over --foreground (via-foreground/40, to-foreground/70), so it reads correctly against any card, hero, or theme without a single hardcoded color.
  • Decorative reduced-motion — meteors are pure ornament: under prefers-reduced-motion they vanish completely (motion-reduce:hidden) instead of freezing, and children are never affected either way.
  • Layer separation — the streak field lives in a single aria-hidden, pointer-events-none layer; content sits in a z-10 layer above it, so decoration never captures clicks or enters the accessibility tree.

On This Page