How to make a glowing border spin

A pure CSS tutorial for a card with a pink, purple and blue gradient border that spins around the edge and glows softly, with a calm static version for people who prefer less motion.

A Get started button with a glowing pink, purple and blue gradient border

In this tutorial you'll build a card with a gradient border that spins around its edge and gives off a soft glow. It's all CSS. No JavaScript, no images, no extra wrapper divs inside the card. The border is a conic gradient, a registered custom property lets the browser animate its angle, and a blurred copy of the same gradient sits behind the card to make the glow. You'll also add a pause on hover and a still version for anyone who has asked their device for less motion.

Start with the card markup

The HTML is as plain as it gets. A wrapper called stage centers things and gives the page a dark background, and the card is a single div with whatever content you like inside.

Everything fancy happens in CSS, so you can drop the same class on an article, a pricing box or a link and it will work the same way.

HTML
<div class="stage">
  <div class="card">
    <h2>Night mode</h2>
    <p>A card with a border that spins around it.</p>
  </div>
</div>

Paint the border with a conic gradient

A normal border can only be one color, so the trick is to give the card a transparent border and paint two backgrounds under it. The first one is a solid color clipped to padding-box, which fills the inside of the card. The second is a conic gradient clipped to border-box, which fills everything, including the strip under the border. Only that 3px strip is left showing, so it looks like a gradient border.

A conic gradient sweeps colors around a center point, like the hands of a clock. Ending on the same pink you started with makes the loop seamless, so you never see a hard line where the colors meet.

The --angle variable sets where the sweep starts. Right now it's 0deg, so nothing moves yet.

CSS
.stage {
  display: grid;
  place-items: center;
  min-height: 100vh;
  margin: 0;
  background: #0b0e1a;
  isolation: isolate;
}

.card {
  --angle: 0deg;
  position: relative;
  width: 280px;
  padding: 28px;
  border: 3px solid transparent;
  border-radius: 18px;
  color: #f4f4f8;
  font: 16px/1.5 sans-serif;
  background:
    linear-gradient(#141828, #141828) padding-box,
    conic-gradient(from var(--angle), #d62a9f, #7c5cff, #38bdf8, #d62a9f) border-box;
}

Register the angle and spin it

Here's the catch. To the browser, a regular custom property is just a string of text. It can't tell that 0deg and 360deg are angles with numbers in between, so it can't animate from one to the other smoothly.

@property fixes that. You register --angle with the syntax '<angle>', set inherits to false and give it an initial value of 0deg. Now the browser knows it's an angle, and a keyframe that moves it to 360deg turns the whole gradient in one smooth loop.

Use linear timing so the speed stays even. Any easing would make the border speed up and slow down on every lap.

CSS
@property --angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

@keyframes spin-border {
  to {
    --angle: 360deg;
  }
}

.card {
  animation: spin-border 4s linear infinite;
}

Add the glow with a blurred copy

For the glow, the card's ::after pseudo-element gets the same conic gradient, sized to the card's outer edge with inset: -3px and blurred with filter: blur(18px). z-index: -1 pushes it behind the card, and the solid inside of the card hides the middle, so only the soft color around the edges peeks out.

Because the angle was registered with inherits: false, the pseudo-element would normally get its own 0deg and sit still. Setting --angle: inherit on ::after tells it to copy the card's current angle, so the glow spins in step with the border.

The isolation: isolate on the stage from earlier matters here. It keeps the glow tucked just behind the card instead of sliding under the page background.

CSS
.card::after {
  --angle: inherit;
  content: '';
  position: absolute;
  inset: -3px;
  z-index: -1;
  border-radius: inherit;
  background: conic-gradient(from var(--angle), #d62a9f, #7c5cff, #38bdf8, #d62a9f);
  filter: blur(18px);
  opacity: 0.7;
}

Pause on hover and respect reduced motion

A nice optional touch is freezing the border when someone hovers the card, so it feels like it's paying attention. animation-play-state: paused holds the angle right where it is, and it picks up from the same spot when the pointer leaves. Changing the duration on hover instead would make the border jump, because the browser recalculates where it should be.

Some people turn on reduced motion in their system settings because constant movement makes them feel unwell. For them, switch the animation off. The gradient border and glow stay, frozen at 0deg, so the card still looks finished.

CSS
.card:hover {
  animation-play-state: paused;
}

@media (prefers-reduced-motion: reduce) {
  .card {
    animation: none;
  }
}

Browser support and the full CSS

@property works in current Chrome, Edge, Safari and Firefox. Firefox was the last to add it, in version 128, which made it Baseline in July 2024.

In an older browser that doesn't know @property, nothing breaks. The --angle: 0deg line on the card keeps the gradient valid, so you still get the gradient border and glow. It just stays still. Here's everything in one block, ready to paste.

CSS
@property --angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

.stage {
  display: grid;
  place-items: center;
  min-height: 100vh;
  margin: 0;
  background: #0b0e1a;
  isolation: isolate;
}

.card {
  --angle: 0deg;
  position: relative;
  width: 280px;
  padding: 28px;
  border: 3px solid transparent;
  border-radius: 18px;
  color: #f4f4f8;
  font: 16px/1.5 sans-serif;
  background:
    linear-gradient(#141828, #141828) padding-box,
    conic-gradient(from var(--angle), #d62a9f, #7c5cff, #38bdf8, #d62a9f) border-box;
  animation: spin-border 4s linear infinite;
}

.card::after {
  --angle: inherit;
  content: '';
  position: absolute;
  inset: -3px;
  z-index: -1;
  border-radius: inherit;
  background: conic-gradient(from var(--angle), #d62a9f, #7c5cff, #38bdf8, #d62a9f);
  filter: blur(18px);
  opacity: 0.7;
}

@keyframes spin-border {
  to {
    --angle: 360deg;
  }
}

.card:hover {
  animation-play-state: paused;
}

@media (prefers-reduced-motion: reduce) {
  .card {
    animation: none;
  }
}

One small tip before you ship it: the blur on the glow spreads about as far as its blur radius, so leave roughly 18px of room around the card. If a parent has overflow: hidden and sits tight around it, the glow gets clipped into a hard edge and the whole effect looks flat.