CSS Rotating Glow Border
A conic-gradient border that spins by animating a registered angle with @property. The blurred glow layer follows the same value automatically.
- Level
- Intermediate
- Published
- Updated
- Reading time
- 5 min
Try it
2 live demos
A border that never stops moving.
A glow that travels around the card
Other ways to build it
Only while hovered
See the codeBuild it
4 snippets to copy
CSS Rotating Glow Border
HTML
<div class="rg-stage">
<div class="rg-card">
<strong>Pro plan</strong>
<p>A border that never stops moving.</p>
</div>
</div>
CSS
/* Registering the variable tells the browser it is an angle,
which is the only reason it can be animated. */
@property --rg-angle {
syntax: "<angle>";
inherits: true;
initial-value: 0deg;
}
/* the glow sits at z-index -1, and this keeps it inside the stage */
.rg-stage {
isolation: isolate;
padding: 2rem;
background: #141415;
}
/* Two backgrounds: a solid fill clipped to the padding box,
and the gradient showing through the transparent border. */
.rg-card {
position: relative;
max-width: 300px;
padding: 1.5rem;
border: 2px solid transparent;
border-radius: 16px;
color: #f0f0f0;
background:
linear-gradient(#141415, #141415) padding-box,
conic-gradient(from var(--rg-angle, 0deg), #b8ff57, #38bdf8, #c084fc, #b8ff57) border-box;
animation: rg-spin 5s linear infinite;
}
/* the same gradient again, blurred, is the glow */
.rg-card::after {
content: "";
position: absolute;
inset: -2px;
z-index: -1;
border-radius: inherit;
background: conic-gradient(from var(--rg-angle, 0deg), #b8ff57, #38bdf8, #c084fc, #b8ff57);
filter: blur(18px);
opacity: .5;
}
@keyframes rg-spin {
to { --rg-angle: 360deg; }
}
@media (prefers-reduced-motion: reduce) {
.rg-card { animation: none; }
}
Only while hovered
The animation starts paused, so the ring holds at its initial angle. Hovering or focusing resumes it from wherever it paused, rather than restarting from zero. Moving away pauses it again. Because the base .rg-card rule already sets animation: none under prefers-reduced-motion, that media query still applies here.
HTML
<a class="rg-card rg-hover" href="#">
<strong>Hover me</strong>
</a>
CSS
/* adds to the .rg-card rules and @keyframes above */
.rg-hover {
display: block;
/* on a link, Chromium stops repainting the ring unless
background-color is set, even to transparent */
background-color: transparent;
animation-play-state: paused;
}
.rg-hover:hover,
.rg-hover:focus-visible {
animation-play-state: running;
}
Understand it
How it works and 5 properties
Custom properties are strings to the browser by default, and string values cannot be interpolated between keyframes. Registering --rg-angle as an <angle> with @property changes that: the browser understands what the value means and can tween it between keyframes, which is what makes the ring spin.
The border is two backgrounds in one declaration: a solid fill covering the card face, and a conic-gradient showing through the transparent border region. A blurred copy of the same gradient sits behind the card as the glow. The glow follows --rg-angle through inheritance: inherits: true in the @property rule passes the animated value down to the ::after pseudo-element.
How it works
The card's background holds two layers. The first is a plain gradient using the surface color on both stops, clipped to padding-box, so it fills the card face and stops at the border. The second is the conic-gradient, clipped to border-box, which covers the full element including the border region. Because the border is declared transparent, the gradient shows through there and nowhere else. The ::after pseudo-element carries the same gradient, blurred and pulled behind the card with z-index: -1. It reads --rg-angle from its parent through normal custom-property inheritance, so one animation drives both layers.
Without @property, a custom property is a plain string, and the browser cannot interpolate a string value. The animation from 0deg to 360deg runs but has no effect. Registering --rg-angle with syntax: "<angle>" tells the browser the value is an angle it can tween. The initial-value: 0deg sets the property's starting point.
background-clip takes a separate value per layer in a multi-layer background. The fill uses padding-box and the gradient uses border-box. Those two clip boundaries are the same unless a border is declared: border: 2px solid transparent opens a 2-pixel gap between them. That transparent strip is where the gradient shows. Without the border declaration, both clip values land at the same edge and no ring appears.
inherits: true in the @property rule lets the ::after glow follow the animation without its own animation declaration. When --rg-angle changes on each frame, child elements that read the property receive the new value through the standard custom-property inheritance chain. Setting inherits: false would leave the pseudo-element at 0deg regardless of the parent animation, and the glow would sit fixed while the border ring turned.
isolation: isolate on the demo wrapper creates a stacking context so the z-index: -1 glow stays behind the card within the wrapper. Without that context, the glow falls behind the wrapper's own background and becomes invisible. isolation is the only property whose sole job is creating that stacking context.
Accessibility notes
The animation stops when prefers-reduced-motion: reduce is set, and --rg-angle falls to 0deg. The border and glow remain visible as a static style.
The rotating border is a visual treatment with no semantic role. The card text is exposed and the pseudo-elements are not.
Where the ring color marks a state such as active or selected, it should meet the 3:1 ratio for non-text graphics under WCAG 1.4.11. In this demo the ring is decorative and that ratio does not apply.
Mistakes worth avoiding
- Forgetting
isolation: isolateon the wrapper. The glow sits atz-index: -1and becomes invisible. - Setting
inherits: falseon the@propertyrule. The::afterglow reads0degpermanently and the ring and glow fall out of sync. - Animating many cards at once without profiling. Each card repaints on every frame because the border involves a gradient, which costs more than a plain
opacityortransformanimation. - Omitting
@media (prefers-reduced-motion: reduce). The continuous spin is the kind of perpetual motion that setting is designed to stop. - Expecting the ring to spin in Safari before 16.4.
@propertyis not supported there;var(--rg-angle, 0deg)uses its fallback and the ring shows at a fixed angle but does not animate.
Ship it
4 features in 4 browsers, 5 questions
Browser support
| Feature | ||||
|---|---|---|---|---|
@property | 85 | 128 | 16.4 | 85 |
conic-gradient() | 69 | 83 | 12.1 | 79 |
filter | 18 | 3.6 | 6 | 13 |
animation | 4 | 5 | 5.1 | 12 |
@property arrived in Firefox 128 and Safari 16.4. conic-gradient() predates it in every engine. filter and animation have been available since well before @property.
What you can build with it
- Highlighted pricing tier. A card for a featured plan that draws the eye through motion without changing the layout.
- Active session indicator. A profile card or device tile that shows which one is currently in use.
- Unread notification card. An alert that has arrived but not been dismissed, where motion conveys pending state.
- Featured item in a product grid. One card that stands out from the rest while the grid stays visually consistent.
- Onboarding step card. A task or prompt the user still needs to complete, where the border draws focus without a tooltip.
Frequently asked questions
Why does the gradient ring not spin without @property?
What does inherits: true do in the @property rule?
::after glow reads --rg-angle through normal inheritance and tracks the border ring angle. Setting inherits: false leaves the glow at 0deg.Why is isolation: isolate on the wrapper instead of the card?
isolation on the card, the glow at z-index: -1 paints over the card's fill rather than sitting behind it. The wrapper is the right location: a stacking context there puts the glow between the wrapper's background and the card face.How does the hover-only variant work?
animation-play-state: paused by default. Hovering or focusing the card switches to running, and the animation resumes from its current position rather than restarting. Removing focus pauses it again at whatever angle it reached.Does this degrade gracefully if @property is not supported?
0deg. The gradient draws, but the keyframe change has no effect: without @property the property carries no type, so the browser cannot interpolate it.