Design engineering

CSS Animated Borders… urgh

First published: 03 May 2025

My kingdom for CSS borders that can actually be animated… There has to be a better way?

I was working on setting up a new animation for the way the DuckDuckGo AI-assisted answers appear. Based on a recent NNg article about AI discoverability and how poor it is in many apps (particularly Amazon's in this case), I recognised that there was an opportunity to help signpost to people that this content had been generated by AI without having to spell it out too much.

In order to do this, there were many, many, many... many... explorations around states we could do. From multiple typing animations, to blurred reveals, to a scanner-style wipe, to a glowing border. That border was where we decided we had something that began to feel right.

Glowing borders aren't new, I mean, Apple Intelligence uses them everywhere, and plenty of other websites and products do various border animations. I explored a multitude of different approaches before landing on a simple outline stroke with transparent edges that would travel around the outside of the module as the content was either generating, or had been finally generated.

CleanShot 2025-05-03 at 17.11.59.gif

Above you can see the final version we landed on for both a cached answer, and a non-cached answer. Notice that the border animation is timed differently depending on each case.

Now, this seems simple and fun. Hell, in Figma you can easily create the look of this in a static mock no problem. But when it comes to implementation, it gets fun and hairy.

The way I chose to implement this was simple in the end, but took a hot minute to get there.

First, create a background wrapper element

.backgroundWrapper {
    position: absolute;
    inset: 0;
    overflow: hidden;
    border-radius: 12px;
}

Then, before that wrapper (so behind it) we apply a conic gradient that we can animate

.backgroundWrapper::before {
    content: '';
    position: absolute;
    inset: -20px;
    background: conic-gradient(
        rgba(37, 106, 248, 0.8),
        rgba(37, 106, 248, 0.6),
        rgba(37, 106, 248, 0.4),
        rgba(37, 106, 248, 0.1),
        rgba(37, 106, 248, 0.4),
        rgba(37, 106, 248, 0.6),
        rgba(37, 106, 248, 0.8)
    );
    filter: blur(24px);
    opacity: 0;

    @media (prefers-color-scheme: dark) {
        background: conic-gradient(
            rgba(114, 149, 246, 0.8),
            rgba(114, 149, 246, 0.6),
            rgba(114, 149, 246, 0.4),
            rgba(114, 149, 246, 0.1),
            rgba(114, 149, 246, 0.4),
            rgba(114, 149, 246, 0.6),
            rgba(114, 149, 246, 0.8)
        );
    }
}

Then above the background wrapper, we need a content layer to cover over the conic gradient, and slightly inset it to leave a gap for the "border" to show through

.backgroundWrapper::after {
    content: '';
    position: absolute;
    inset: 1.5px;
    background: var(--theme-assist-bg-chat-system);
    border-radius: calc(var(--sds-radius-x03) - 1px);
}


/* Content layer */
.content {
    position: relative;
    z-index: 1;
}

Then, we animate the glow by rotating the conic gradient

@keyframes rotate-glow {
    0% {
        transform: rotate(0deg);
        opacity: 0;
    }
    15% {
        opacity: 1;
    }
    85% {
        opacity: 1;
    }
    100% {
        transform: rotate(180deg);
        opacity: 0;
    }
}

@keyframes rotate-glow-continuous {
    0% {
        transform: rotate(0deg);
        opacity: 0;
    }
    15% {
        opacity: 1;
    }
    85% {
        opacity: 1;
    }
    100% {
        transform: rotate(360deg);
        opacity: 0;
    }
}

@keyframes completion-glow {
    0% {
        opacity: 0;
        filter: blur(24px);
    }
    50% {
        opacity: 1;
        filter: blur(24px);
    }
    100% {
        opacity: 0;
        filter: blur(24px);
    }
}

.animateBorder .backgroundWrapper::before {
    animation: rotate-glow 1s linear forwards;
}

.animateBorderContinuous .backgroundWrapper::before {
    animation: rotate-glow-continuous 2.5s linear infinite;
}

.animateCompletion .backgroundWrapper::before {
    animation: completion-glow 2s ease-out forwards;
    animation-delay: 200ms;
    background: var(--sds-color-background-accent-01);
}

This... this is how complicated it was to achieve what I wanted. Why can't I just directly animate a border with some simple properties? The below is just a rough example, but yeah, why not?

.borderAnimation {
	border: 
		1px,
		linear-gradient(
        	rgba(37, 106, 248, 0.1),
        	rgba(37, 106, 248, 1),
        	rgba(37, 106, 248, 0.1)
    	);
	animation: rotate-glow 1s linear forwards;
};
Last updated: 05 May 2025 (about 18 hours ago)
Subscribe

Get an email whenever I publish a new thought.

Or you can subscribe via RSS
More to explore in design engineering