Hyperiux Vault

Milky Way

Component 92

About

GPU-accelerated GPGPU particle galaxy with spiral arms, curl-noise nebula smoke, twinkling sparkle stars, background starfield, and post-processing vignette + edge blur

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Milky Way to your project and fits it to your theme.

Code

How Milky Way is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import { useEffect, useRef, useState } from "react";
import gsap from "gsap";
import MilkyWay from "@/components/ui/milky-way";

/* ------------------------------------------------------------------ *
 * Small rotating "explore" badge — spins continuously, speeds up on
 * hover. Presentational chrome for this demo, not part of the effect.
 * ------------------------------------------------------------------ */
function CircularText({
  text = "",
  spinDuration = 20,
  radius = 64,
}: {
  text?: string;
  spinDuration?: number;
  radius?: number;
}) {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const tweenRef = useRef<gsap.core.Tween | null>(null);
  const letters = Array.from(text);

  useEffect(() => {
    if (!containerRef.current) return;
    tweenRef.current?.kill();

    if (window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches) {
      return;
    }

    tweenRef.current = gsap.to(containerRef.current, {
      rotation: 360,
      duration: spinDuration,
      ease: "none",
      repeat: -1,
      transformOrigin: "50% 50%",
    });

    return () => {
      tweenRef.current?.kill();
    };
  }, [spinDuration, text]);

  const handleEnter = () => {
    if (window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches) return;
    if (!tweenRef.current) return;
    gsap.to(tweenRef.current, { timeScale: 2, duration: 0.3 });
  };

  const handleLeave = () => {
    if (window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches) return;
    if (!tweenRef.current) return;
    tweenRef.current.resume();
    gsap.to(tweenRef.current, { timeScale: 1, duration: 0.3 });
  };

  return (
    <div style={{ position: "relative" }}>
      <span
        style={{
          position: "absolute",
          left: "50%",
          top: "50%",
          width: 44,
          height: 44,
          transform: "translate(-50%, -50%)",
          zIndex: 2,
          userSelect: "none",
          pointerEvents: "none",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
        }}
      >
        <svg width={22} height={22} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M12 3l1.9 5.8L20 11l-6.1 2.2L12 19l-1.9-5.8L4 11l6.1-2.2z" />
        </svg>
      </span>

      <div
        ref={containerRef}
        onMouseEnter={handleEnter}
        onMouseLeave={handleLeave}
        style={{ width: radius * 2 + 20, height: radius * 2 + 20, position: "relative" }}
      >
        {letters.map((letter, i) => {
          const angle = (360 / letters.length) * i;
          return (
            <span
              key={i}
              style={{
                position: "absolute",
                left: "50%",
                top: "50%",
                transform: `rotate(${angle}deg) translate(${radius}px) rotate(90deg)`,
                transformOrigin: "0 0",
                fontSize: 15,
                fontWeight: 600,
                whiteSpace: "pre",
                pointerEvents: "none",
                zIndex: 3,
              }}
            >
              {letter}
            </span>
          );
        })}
      </div>
    </div>
  );
}

export default function MilkyWayDemo() {
  // Hide the spinning badge on touch devices — the component itself swaps to
  // a plain "open on desktop" message there, and there's no cursor to invite.
  const [isCoarsePointer, setIsCoarsePointer] = useState(false);
  useEffect(() => {
    const mq = window.matchMedia("(pointer: coarse)");
    const update = () => setIsCoarsePointer(mq.matches);
    update();
    mq.addEventListener("change", update);
    return () => mq.removeEventListener("change", update);
  }, []);

  return (
    <div className="relative h-screen w-full overflow-hidden bg-black">
      <MilkyWay particleSize={0.6} rotationSpeed={0.35} mouseInfluence />

      <div className="pointer-events-none absolute inset-x-0 bottom-0 z-10 flex items-end justify-between gap-6 p-[2vw] text-white">
        <div className="max-w-sm">
          <p className="text-xs uppercase tracking-[0.2em] text-white/50">
            WebGL &middot; GPGPU particles
          </p>
          <h1 className="mt-1 text-2xl font-medium sm:text-3xl">Milky Way</h1>
          <p className="mt-1 text-sm leading-relaxed text-white/60">
            A GPU-simulated spiral galaxy with drifting nebula smoke and a
            starfield backdrop — tilts toward the cursor.
          </p>
        </div>

        {!isCoarsePointer && (
          <div className="pointer-events-auto shrink-0 cursor-pointer text-white/80">
            <CircularText text="EXPLORE THE GALAXY " spinDuration={20} radius={64} />
          </div>
        )}
      </div>
    </div>
  );
}

Dependencies

More by Hyperiux Vault

View all
  • Product Timeline

    Product Timeline

    2.2K
  • Stack Spread

    Stack Spread

    1.3K
  • Arrow Fill Button

    Arrow Fill Button

    908
  • Interactive List Preview

    Interactive List Preview

    866
  • Shiny Button

    Shiny Button

    733
  • Circular Split Roll

    Circular Split Roll

    632
  • Zoom Slider

    Zoom Slider

    423
  • Stack Loader

    Stack Loader

    370

Explore more

Part of Hyperiux Vault — browse the full library on 21st.dev.