How to Create Animated Gradient Borders in CSS

- 6 min read

Custom Border Animations

This article is adapted from a chapter of my book, You Don’t Need JavaScript.

Animated borders used to require heavy tricks: JavaScript continuously redrawing gradients, or SVG filters layered behind content. Today, modern CSS gives us all the ingredients natively. With a pseudo-element, a conic gradient, and the new @property rule, we can create glowing, rotating borders that feel alive.

Final result: glowing border moving indefinitely
Final result: glowing border moving indefinitely

The Basic HTML Structure

The markup is minimal:

<div class="card">
	<!-- Your card content goes here -->
</div>

The work all happens in CSS.

Preparing the card

The card needs to establish itself as a positioning context so that its pseudo-elements can sit exactly behind it. It also needs a solid background to prevent the animated gradient from leaking through.

.card {
	position: relative;
	z-index: 1; /* keep content above the border */
	background-color: white; /* hides gradient behind the face */
}

Without the background color, the gradient layer we’ll add would bleed through, competing with the card’s content.

Result if you don’t add a background to the card
Result if you don’t add a background to the card

Adding a border layer

We create the “border” not with border itself, but with a pseudo-element stretched slightly larger than the card.

.card::after {
	content: '';
	position: absolute;
	inset: -4px; /* expand 4px beyond every edge */
	z-index: -1; /* push it behind the card */
	border-radius: inherit;
}

The negative inset makes the pseudo-element stick out beyond the card, visually becoming its border. Inheriting the radius ensures corners line up perfectly.

Declaring an animatable property

We want this border to rotate smoothly. To do that, we need a custom property for the angle — but not all custom properties can animate by default. The @property at-rule tells the browser that —angle should behave like a real angle value, so it can be interpolated in keyframes.

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

This registers —angle as an angle type, starting at 0deg. The inherits: false ensures each card manages its own angle, rather than inheriting from a parent.

Drawing the gradient

Now we paint the border using a conic gradient that rotates based on —angle:

.card::after {
	/* ...previous declarations... */
	background: conic-gradient(
		from var(--angle),
		#ff4545,
		#00ff99,
		#006aff,
		#ff0095,
		#ff4545 /* repeat first stop for a seamless loop */
	);
}

The repeating first color stop avoids a visible jump where the gradient loops. Even without animation, this already produces a colorful border.

Gradient border result (no glowing)
Gradient border result (no glowing)

Animating the spin

To bring it to life, animate —angle from 0 to 360 degrees in an infinite loop:

.card::after {
	/* ...previous declarations... */
	animation: spin 3s linear infinite;
}

@keyframes spin {
	to {
		--angle: 360deg;
	}
}
smooth, endlessly rotating gradient border
smooth, endlessly rotating gradient border
You don't need JavaScript ebook cover

I wrote a guide about building modern UI with HTML and CSS first. It covers practical patterns you can use before reaching for JavaScript.

Learn more

Adding the glow

The border already rotates, but we can make it feel more alive by giving it a soft halo. We do this by duplicating the gradient in another pseudo-element and applying blur.

/* Shared border setup for both layers */
.card::after,
.card::before {
	content: '';
	position: absolute;
	inset: -4px;
	z-index: -1;
	border-radius: inherit;
	background: conic-gradient(from var(--angle), #ff4545, #00ff99, #006aff, #ff0095, #ff4545);
	animation: spin 3s linear infinite;
}

/* Blur and fade the ::before layer for a glow effect */
.card::before {
	filter: blur(1.5rem);
	opacity: 0.8;
}

Here’s what’s happening in layers:

The ::after pseudo-element is our crisp, sharp border.

The ::before pseudo-element is the exact same gradient, but blurred and semi-transparent. The blur makes the bright colors spill outward beyond the edge, softening into a glow.

Because both are positioned in the same place, the sharp version sits on top and the blurred version radiates beneath it.

Together, they produce the illusion of a glowing border without any extra graphics or images.

Border + glow effect result
Border + glow effect result

Respecting reduced motion

Animations should never be forced on users who prefer static interfaces. We can turn the spin off gracefully with a media query:

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

This leaves a colorful static border for users who opt out of motion.

Final thoughts

With modern CSS, custom border animations no longer need JavaScript workarounds or SVG tricks. A pseudo-element, a conic gradient, and @property are enough to create a border that feels polished, animated, and surprisingly lightweight.

Like any strong visual effect, it works best when used with intention. Keep it for featured surfaces, and keep the reduced-motion fallback in place so the effect stays decorative rather than distracting.

You can check a codepen here: https://codepen.io/editor/theosoti/pen/019d123a-1628-7b30-967f-8ce6e24ddd87.

Or you can check out a live example on my landing page: https://theosoti.com/you-dont-need-js/.

Support My Work

Feel free to leave your comments or questions by email at hey@theosoti.com.
If you found this article helpful, please share it with your peers and join my newsletter below for more web development tutorials.

Happy coding!

    No strings attached, unsubscribe anytime!

    Other articles you might like

    Explore more articles on front-end development, covering HTML, CSS and JavaScript for building high-performance websites.