Zum Inhalt springen
UI Kit
Accent

Sternschnuppen

Feine Streifen, die schräg über die Bühne ziehen, aus festen Startpunkten über leisen Sternen.

Eine von 6 Components in Beams, Partikel und Bewegung

3 Variants

Variants

Schauer

Viele Streifen mit Akzentkopf, für eine Launch-Nacht.

Ruhiger Himmel

Wenige lange, langsame Streifen in der Textfarbe, die Sterne ziehen beim Scrollen wie ein ferner Himmel.

Steil

Die Streifen fallen fast senkrecht, der Winkel ist frei wählbar.

Details

Was Sternschnuppen mitbringt, aus Registry und Quellcode gelesen.

Bereich
Elements
Variants
3
Version
1.0.0 · seit 1.0.0
Datei
components/ui/meteors.tsx
Bringt mit
utilskit
npm-Pakete
Keine
Sprachen
Texte kommen über Props
Studio
Im Code anpassen

Props

Die Schnittstelle MeteorsProps aus components/ui/meteors.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • countoptional
    number

    Number of streaks, default 12

  • angleoptional
    number

    Direction of travel in degrees on screen, 0 is to the right, 90 straight down. Default 135 (down to the left).

  • speedoptional
    number

    Average seconds between two passes of one streak, default 10

  • lengthoptional
    number

    Tail length in px, default 140

  • toneoptional
    'accent' | 'neutral'

    accent tints the head, neutral stays in the text color

  • starsoptional
    number

    Faint fixed stars behind the streaks, default 40

  • seedoptional
    number

    Same seed, same sky

  • parallaxoptional
    number

    Pixels the field of stars drifts against the page over a full scroll pass, like a far sky. Default 0 (off), 40 reads as depth.

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.

Bewegung

  • Bewegt sich mit dem Scrollen, der Fortschritt folgt der Position auf der Seite.
  • Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
  • Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig.

Quellcode

Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.

Datei anzeigen, 158 Zeilen
components/ui/meteors.tsx
'use client';

import { useEffect, useRef } from 'react';
import { cn, prefersReducedMotion, round } from '@/lib/kit';

export interface MeteorsProps {
  /** Number of streaks, default 12 */
  count?: number;
  /** Direction of travel in degrees on screen, 0 is to the right, 90 straight down. Default 135 (down to the left). */
  angle?: number;
  /** Average seconds between two passes of one streak, default 10 */
  speed?: number;
  /** Tail length in px, default 140 */
  length?: number;
  /** accent tints the head, neutral stays in the text color */
  tone?: 'accent' | 'neutral';
  /** Faint fixed stars behind the streaks, default 40 */
  stars?: number;
  /** Same seed, same sky */
  seed?: number;
  /** Pixels the field of stars drifts against the page over a full scroll pass, like a far sky. Default 0 (off), 40 reads as depth. */
  parallax?: number;
  className?: string;
}

const CSS = `
@keyframes kitfx-meteor {
  0% { transform: translate3d(0, 0, 0); opacity: 0; }
  4% { opacity: 1; }
  32% { opacity: 1; }
  40%, 100% { transform: translate3d(var(--kitfx-d), 0, 0); opacity: 0; }
}
.kitfx-meteors * { animation-play-state: var(--kitfx-play, running); }
@media (prefers-reduced-motion: reduce) {
  .kitfx-meteors-sky { transform: none !important; }
  .kitfx-meteors [data-meteor] { animation: none !important; transform: translate3d(var(--kitfx-still), 0, 0); opacity: var(--kitfx-still-o); }
}
`;

function seeded(seed: number) {
  let a = seed >>> 0 || 1;
  return () => {
    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;
  };
}

/**
 * Scroll pass. While `on`, writes --kitfx-p on the element: 0 when its top
 * meets the bottom of the window, 1 when its bottom leaves at the top. Never
 * with reduced motion, the CSS then falls back to the resting value.
 */
function useScrollPass(ref: { current: HTMLElement | null }, on: boolean) {
  useEffect(() => {
    const el = ref.current;
    if (!el || !on || prefersReducedMotion()) return;
    let frame = 0;
    const update = () => {
      frame = 0;
      const r = el.getBoundingClientRect();
      const p = Math.min(1, Math.max(0, (window.innerHeight - r.top) / (r.height + window.innerHeight)));
      el.style.setProperty('--kitfx-p', p.toFixed(4));
    };
    const onScroll = () => {
      if (!frame) frame = requestAnimationFrame(update);
    };
    update();
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', onScroll);
    return () => {
      cancelAnimationFrame(frame);
      window.removeEventListener('scroll', onScroll);
      window.removeEventListener('resize', onScroll);
      el.style.removeProperty('--kitfx-p');
    };
  }, [ref, on]);
}

/**
 * Thin streaks that cross the stage on a slant, from seeded positions so
 * server and browser agree. Fills its parent, decorative. With reduced
 * motion a few streaks stand still mid flight, the rest stay hidden.
 */
export function Meteors({ count = 12, angle = 135, speed = 10, length = 140, tone = 'accent', stars = 40, seed = 5, parallax = 0, className }: MeteorsProps) {
  const ref = useRef<HTMLDivElement>(null);
  useScrollPass(ref, parallax !== 0);
  useEffect(() => {
    const el = ref.current;
    if (!el || typeof IntersectionObserver === 'undefined') return;
    const io = new IntersectionObserver(([e]) => el.style.setProperty('--kitfx-play', e.isIntersecting ? 'running' : 'paused'));
    io.observe(el);
    return () => io.disconnect();
  }, []);

  const rnd = seeded(seed);
  const rad = (angle * Math.PI) / 180;
  // Start on the edges the streaks come from, spread so they cross the whole stage
  const fromRight = Math.cos(rad) < 0;
  const meteors = Array.from({ length: count }, (_, i) => {
    const along = rnd();
    const onTop = rnd() < 0.72;
    const x = onTop ? round(-10 + along * 120) : fromRight ? 104 : -4;
    const y = onTop ? -4 : round(along * 70);
    const dur = round(speed * (0.7 + rnd() * 0.7), 1);
    return {
      x,
      y,
      dur,
      delay: round(-rnd() * dur, 1),
      dist: Math.round(700 + rnd() * 600),
      len: Math.round(length * (0.6 + rnd() * 0.7)),
      show: i % 3 === 0,
      still: round(0.12 + rnd() * 0.2),
    };
  });
  const sky = Array.from({ length: stars }, () => ({ x: round(rnd() * 100), y: round(rnd() * 100), s: rnd() > 0.85 ? 2 : 1, o: round(0.25 + rnd() * 0.6) }));
  const head = tone === 'accent' ? 'color-mix(in oklab, var(--primary) 80%, var(--foreground))' : 'var(--foreground)';

  return (
    <div ref={ref} aria-hidden data-clip-ok className={cn('kitfx-meteors pointer-events-none absolute inset-0 overflow-hidden', className)}>
      <style dangerouslySetInnerHTML={{ __html: CSS }} />
      <div className="kitfx-meteors-sky absolute inset-0" style={parallax ? { transform: `translate3d(0, calc((var(--kitfx-p, 0.5) - 0.5) * ${parallax}px), 0)` } : undefined}>
        {sky.map((s, i) => (
          <span
            key={`s${i}`}
            className="absolute rounded-full bg-foreground"
            style={{ left: `${s.x}%`, top: `${s.y}%`, width: s.s, height: s.s, opacity: round(s.o * 0.3) }}
          />
        ))}
      </div>
      {meteors.map((m, i) => (
        <div key={i} className="absolute" style={{ left: `${m.x}%`, top: `${m.y}%`, transform: `rotate(${angle}deg)`, transformOrigin: '0 0' }}>
          <div
            data-meteor=""
            className="relative h-px opacity-0"
            style={{
              width: m.len,
              marginLeft: -m.len,
              ['--kitfx-d' as string]: `${m.dist}px`,
              ['--kitfx-still' as string]: `${Math.round(m.dist * m.still)}px`,
              ['--kitfx-still-o' as string]: m.show ? 0.8 : 0,
              animation: `kitfx-meteor ${m.dur}s linear ${m.delay}s infinite`,
            }}
          >
            {/* tail fades out behind the head, the head carries a small glow */}
            <span className="absolute inset-0 rounded-full opacity-60 dark:opacity-80" style={{ background: `linear-gradient(to right, transparent, color-mix(in oklch, ${head} 70%, transparent))` }} />
            <span
              className="absolute right-0 top-1/2 size-[3px] -translate-y-1/2 rounded-full"
              style={{ background: head, boxShadow: `0 0 0 1px color-mix(in oklch, ${head} 18%, transparent), 0 0 10px 2px color-mix(in oklch, ${head} 40%, transparent)` }}
            />
          </div>
        </div>
      ))}
    </div>
  );
}

Häufige Fragen

Wie installiere ich Sternschnuppen?

Mit aiw add @ai-website/meteors. Der Befehl legt die Datei components/ui/meteors.tsx in dein Projekt, der Code gehört danach dir. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat Sternschnuppen?

3 Varianten, Schauer, Ruhiger Himmel und Steil.

Kann ein KI-Assistent Sternschnuppen einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Sternschnuppen mit aiw_components und installieren sie mit aiw_add @ai-website/meteors.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert