Skip to content
← components

Galaxy Button

new

A glowy CTA with orbiting stars, a conic spark sweep, and a 3D star ring built on transform-style: preserve-3d. Lights up on hover. Ported from jh3y.

hover or focus to light it up

Usage

example.tsx
import { GalaxyButton } from "~/components/buttons/galaxy-button";
<GalaxyButton onClick={explore}>Explore</GalaxyButton>
// custom icon
import { Rocket } from "lucide-react";
<GalaxyButton icon={<Rocket />}>Launch</GalaxyButton>

How it works

This one's all jh3y. The clever bit, and the reason it feels like more than a glow, is the star ring. The button sets transform-style: preserve-3d and a perspective, then a flat circular element gets tipped back with a few rotateX/rotateY transforms so it reads as a disc receding into space. The stars are absolutely-positioned dots pushed out along that disc and spun with a linear orbit animation, so they trace an ellipse instead of a flat circle.

Everything else hangs off a single --active variable that flips from 0 to 1 on hover or focus. The glow, the scale, the star opacity, the conic spark sweep, all of it interpolates from that one number, so the whole thing brightens together. I scoped it to the button (the original toggled the page background via :has), kept a real <button> for focus, and parked the animations behind prefers-reduced-motion.

galaxy-button.css
/* One --active variable drives everything: glow, scale, star opacity. */
.galaxy-btn {
--active: 0;
transform-style: preserve-3d; /* the 3D ring lives in here */
perspective: 100vmin;
scale: calc(1 + var(--active) * 0.1);
box-shadow: 0 0 calc(var(--active) * 6em) calc(var(--active) * 3em)
hsl(var(--hue) 97% 61% / .5);
}
.galaxy-btn:is(:hover, :focus-visible) { --active: 1; }
/* The ring is a flat circle tipped into 3D; stars orbit around it. */
.galaxy__ring {
transform: rotateX(-24deg) rotateY(-30deg) rotateX(90deg);
transform-style: preserve-3d;
}
.star {
transform: translate(-50%, -50%) rotate(10deg)
translateY(calc(var(--distance) * 1px));
animation: galaxy-orbit calc(var(--duration) * 1s) infinite linear;
}

Props

proptypedefaultdescription
childrenReactNode"Explore"Button label.
iconReactNode<Sparkles />Leading icon. Pass null to omit.
…propsButtonHTMLAttributesAny native button prop (onClick, type, aria-*).

Original by jh3y.