---
title: "The galaxy button: orbiting stars and a 3D ring in CSS"
description: "jh3y's glowy CTA, ported to a React component. Orbiting stars, a conic spark sweep, and a star ring tipped into 3D with transform-style: preserve-3d, all driven by one --active variable."
date: "2026-05-05"
tags: ["css", "3d", "animation", "buttons"]
---

[jh3y](https://x.com/jh3yy/status/1644055788023939073) posted a button that I
couldn't stop hovering. It's a glow, sure, but there are tiny stars orbiting
*around* it on a tilted ring, like the button is a little planet. Hover it.

## The ring is the trick

A glow is easy. The part that makes this feel three-dimensional is the star
ring. The button sets `transform-style: preserve-3d` and a `perspective`, then a
flat circular element is tipped back with a stack of rotations so it reads as a
disc receding into the distance:

```css
.galaxy__ring {
  transform: rotateX(-24deg) rotateY(-30deg) rotateX(90deg);
  transform-style: preserve-3d;
}
```

The stars are just absolutely-positioned dots pushed outward along that disc and
spun with a linear animation. Because they live inside a 3D-transformed parent,
their circular orbit projects to an ellipse, which is what your eye reads as
"around" rather than "on top of."

```css
.star {
  transform: translate(-50%, -50%) rotate(10deg)
             translateY(calc(var(--distance) * 1px));
  animation: galaxy-orbit calc(var(--duration) * 1s) infinite linear;
}
```

## One variable runs the show

Every other piece, the glow, the scale, the star opacity, the conic spark
sweeping around the rim, hangs off a single `--active` value that flips from 0
to 1 on hover or focus:

```css
.galaxy-btn { --active: 0; scale: calc(1 + var(--active) * 0.1); }
.galaxy-btn:is(:hover, :focus-visible) { --active: 1; }
```

Because each effect multiplies by `--active`, they all wake up together and
settle together. No JavaScript, no keyframe juggling, just one number and a
`transition`.

Porting it to React, I kept it honest: a real `<button>` so focus works, the
whole thing scoped to the component (the original toggled the page background
through `:has`), the stars hard-coded so server and client render the same, and
the animations parked behind `prefers-reduced-motion`.

Full credit to [jh3y](https://codepen.io/jh3y/pen/eYPYKep) for the original.
Grab the component on the [Galaxy Button](/components/galaxy-button) page.
