---
title: "Animated Gradient Borders in CSS: 3 Techniques"
description: "Three ways to build a gradient border in CSS, with live demos: border-image, clipped backgrounds and a conic pseudo-element, animated with @property."
url: https://frontendhelpers.com/css-animated-gradient-border
updated: 2026-10-12
---

# Animated gradient borders: three CSS techniques

border-image, clipped backgrounds and a conic pseudo-element — which ones handle rounded corners, how to make them move, and what each costs.

CSS recipe · By [AurumCodeWorks](https://frontendhelpers.com/about) · 7 min read · Updated 12 October 2026

A gradient border looks as though it should be one property, and it isn't. `border-color` only accepts colours, so there is no `border-color: linear-gradient(…)`. You have to borrow another feature — the border image, the background, or a pseudo-element — and that choice decides whether rounded corners work, whether the inside can stay transparent, and how much the animation costs each frame. Here are the three techniques running live, then the exact CSS behind each.

Square corners

**1\. border-image** — static; `border-radius: 14px` is set but ignored

**2\. Clipped backgrounds** — rounded, spun with an `@property` angle, solid focus ring

Rotating glow

**3\. Conic pseudo-element** — rounded, rotated with `transform`, static glow

All three use the same stops — violet `#8b5cf6`, cyan `#06b6d4` and emerald `#10b981` — which you can rework in our [CSS gradient generator](https://frontendhelpers.com/css-gradient-generator) before pasting them in. If what you actually want is a gradient fill that shifts on hover rather than a border, the [Gradient Shift card](https://frontendhelpers.com/css-hover-effects#anim-hover-gradient) in our hover effects library is a simpler starting point.

## Technique 1: border-image

`border-image` was designed to slice an image into nine pieces and paint them into the border area. As far as CSS is concerned a gradient is an image, so you can pass one in directly:

```css
.gb-image {
  padding: 18px 26px;
  border: 4px solid;
  border-image: linear-gradient(90deg, #8b5cf6, #06b6d4, #10b981) 1;
  border-radius: 14px; /* has no effect on the border image */
  color: #f8fafc;
  font-weight: 600;
}
```

The trailing `1` is `border-image-slice`. Leave it out and the default slice of 100% hands the whole gradient to the four corner regions, leaving the edges empty. You still need a `border-style` and width, because the image is drawn into the border area and an element with no border has none.

The catch is spelled out in the [MDN reference for border-image](https://developer.mozilla.org/en-US/docs/Web/CSS/border-image): `border-radius` has no effect on the border image. A border image can be pushed outside the border box with `border-image-outset`, so clipping it to the curve wouldn't make sense. The first demo sets a 14px radius and the corners stay square. Worse, the radius still rounds any background or `box-shadow` on the same element, so mixing them looks broken.

That makes `border-image` the right tool for square-cornered things: callout bars, table headers, or a single accent edge with `border-width: 0 0 0 4px`. It's also the only technique here that leaves the element's interior completely untouched, so photos, patterns and backdrop filters behind it show through. MDN lists it as widely available since July 2015.

## Technique 2: two backgrounds, two clip boxes

For rounded corners, draw the border with the background instead. Make the border transparent so the background shows through it, then stack two layers: a solid fill clipped to the padding box on top, and the gradient clipped to the border box underneath. The gradient is only visible in the ring the fill doesn't cover, and because backgrounds follow `border-radius`, the ring curves with the corners. MDN offers this same pattern as its workaround on the border-image page.

```css
@property --gb-angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}
.gb-clip {
  padding: 16px 26px;
  border: 3px solid transparent;
  border-radius: 14px;
  background:
    linear-gradient(#0b1222, #0b1222) padding-box,
    conic-gradient(from var(--gb-angle, 0deg), #8b5cf6, #06b6d4, #10b981, #8b5cf6) border-box;
  animation: gb-spin 4s linear infinite;
  color: #f8fafc;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.gb-clip:focus-visible {
  outline: 2px solid #f8fafc;
  outline-offset: 4px;
}
@keyframes gb-spin {
  to { --gb-angle: 360deg; }
}
```

The keyword after each layer does double duty. In the `background` shorthand, a single box keyword sets both `background-origin` and `background-clip`. The origin matters: it defaults to the padding box, so without `border-box` the gradient would be sized to the inside and tile underneath the border, leaving a visible seam along the edges.

The fill is written as `linear-gradient(#0b1222, #0b1222)` on purpose. A background colour can only belong to the bottom layer, so a solid fill that sits on top of another layer has to be expressed as a one-colour gradient image.

The limitation: the top layer must be opaque and match the element's intended interior, so this can't give you a see-through card over a photo. The inner curve is automatic — 11px here, the 14px radius minus the 3px border.

## Technique 3: a conic-gradient pseudo-element

For a glow or a radar-style sweep, move the gradient onto its own layer. The third demo puts an oversized square `::before` behind the content, fills it with a `conic-gradient`, and covers the middle with an `::after` inset by the border width:

```css
.gb-glow {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding: 18px 26px;
  border-radius: 14px;
  box-shadow: 0 0 22px rgba(6, 182, 212, 0.25);
  color: #f8fafc;
  font-weight: 600;
}
.gb-glow::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 150%;
  aspect-ratio: 1;
  z-index: -2;
  background: conic-gradient(#8b5cf6, #06b6d4, #10b981, #8b5cf6);
  transform: translate(-50%, -50%) rotate(0turn);
  animation: gb-rotate 4s linear infinite;
}
.gb-glow::after {
  content: "";
  position: absolute;
  inset: 3px;
  z-index: -1;
  border-radius: 11px;
  background: #0b1222;
}
@keyframes gb-rotate {
  to { transform: translate(-50%, -50%) rotate(1turn); }
}
```

The parent's `overflow: hidden` and `border-radius` trim the spinning square into a rounded ring. `isolation: isolate` creates a stacking context, so the negative `z-index` layers stay inside the element instead of dropping behind the page. The square is 150% of the element's width so that its corners always cover the element's corners mid-turn; for an element taller than it is wide, size it from the height instead, or you'll see empty corners flicker past. The glow is a static `box-shadow`.

If the interior must stay transparent, replace the cover layer with a mask. This variant isn't one of the demos above; it uses a single `::before` with padding equal to the border width and the same `gb-spin` keyframes:

```css
.gb-mask {
  position: relative;
  border-radius: 14px;
}
.gb-mask::before {
  content: "";
  position: absolute;
  inset: 0;
  padding: 3px; /* border thickness */
  border-radius: inherit;
  background: conic-gradient(from var(--gb-angle, 0deg), #8b5cf6, #06b6d4, #10b981, #8b5cf6);
  mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
  animation: gb-spin 4s linear infinite;
  pointer-events: none;
}
```

The first mask layer covers only the content box and the second covers everything; `exclude` keeps the areas where exactly one layer is present, which is the padding ring. The [MDN page for mask-composite](https://developer.mozilla.org/en-US/docs/Web/CSS/mask-composite) shows it available across browsers since December 2023, so the prefixed `-webkit-mask-composite: xor` in older tutorials is only needed for browsers from before then.

## Making it move: @property or transform

Gradients can't be interpolated. A `conic-gradient()` is an image, `background-image` animates discretely, so keyframes that swap one gradient for another snap rather than tween. An ordinary custom property can't help either: until it's registered, the browser treats its value as a string of tokens with no type to interpolate.

Registering it with `@property` gives it one. Declared as an `<angle>`, `--gb-angle` tweens from `0deg` to `360deg`, and the gradient that reads it is recomputed each frame. The [MDN reference for @property](https://developer.mozilla.org/en-US/docs/Web/CSS/@property) marks it Baseline 2024, working across the latest browsers since July 2024. Both `syntax` and `inherits` are required, as is `initial-value` for any syntax other than `*`; leave one out and the whole rule is ignored. `inherits: false` keeps the angle from cascading into the button's children.

Note the fallback in `var(--gb-angle, 0deg)`. In a browser without `@property` the property has no value outside the keyframes, and without a fallback the whole `background` declaration would be invalid at computed-value time: no fill and no border. With it, older browsers get a static gradient. The same approach animates technique 1 too, since `border-image` accepts a gradient that reads a variable.

The other option is never to animate the gradient at all. Technique 3 paints its [conic gradient](https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/conic-gradient) once and spins the layer with `transform: rotate()`. For a conic sweep the result looks almost identical, but it only works when the gradient lives on a layer that can turn by itself — you can't rotate a border image or one background layer independently of the element's content.

## The three techniques compared

| Technique | Rounded corners | Animatable how | Extra element | Support / caveats |
| --- | --- | --- | --- | --- |
| `border-image` | No — `border-radius` is ignored | `@property` angle inside the gradient (repaints) | None | Widely available since 2015; interior stays transparent; needs a border style and the `1` slice |
| Clipped backgrounds | Yes | `@property` angle (repaints) | None | Inner fill must be an opaque colour that matches the design; `@property` is Baseline 2024 |
| Conic pseudo-element | Yes, via `overflow: hidden` or a mask | `transform: rotate()` (composited) or `@property` | `::before`, plus `::after` or a mask | Needs positioning and `isolation`; square must outsize the box; `mask-composite` since December 2023 |

## Performance: what repaints and what composites

Animating `--gb-angle` changes a value the background depends on, so on every frame the browser recalculates style and repaints that element's background on the main thread. For one button that's a small area and comfortably within the frame budget — the same cost class as animating `background-color`, which our [CSS animation performance guide](https://frontendhelpers.com/css-animation-performance) covers in detail.

Rotating a pseudo-element is different. The gradient is painted once, the layer goes to the compositor, and each frame is just a rotation of an existing texture. It stays smooth even while JavaScript keeps the main thread busy.

Two practical rules follow. Keep animated borders few and small: a spinning border on one call to action is fine, but on every card in a grid of thirty it becomes thirty repaints per frame. And check, rather than guess: turn on Paint flashing in Chrome's Rendering panel. The `@property` demo flashes continuously; the transform demo shouldn't.

## Accessibility: reduced motion and focus

A border that loops forever is exactly the kind of motion `prefers-reduced-motion` exists for. This page's stylesheet switches the animations off for anyone who has asked their system for less motion:

```css
@media (prefers-reduced-motion: reduce) {
  .gb-clip,
  .gb-glow::before { animation: none; }
}
```

The angle falls back to its `initial-value` and the pseudo-element to its resting transform, so both demos become static gradient borders rather than disappearing. Turn on reduced motion in your operating system and reload to check.

Don't let the moving border be the focus indicator. It's already animating before the button receives focus, so its appearance doesn't change when focus arrives and a keyboard user gets no signal. Colour is the other risk: WCAG 1.4.11 asks for 3:1 contrast against adjacent colours, and a gradient is only as strong as its weakest stop. On this dark page all three stops pass, the violet narrowly at about 4.7:1. Put the same gradient on white and the cyan stop measures about 2.4:1. Our [WCAG colour contrast guide](https://frontendhelpers.com/wcag-colour-contrast-guide) explains how to measure it. The demo button therefore keeps its gradient and adds a solid near-white `:focus-visible` outline, offset so it sits on the page background, where it measures about 19:1.

## Which one to use

Square corners or a transparent interior on a simple box: `border-image`. Rounded corners on a solid button or card: clipped backgrounds — the least code, with no extra element. A glow, a sweep or anything that animates continuously: the pseudo-element, rotated with `transform` so the compositor does the work. Whichever you pick, more ideas for interactive states live in our [CSS hover effects](https://frontendhelpers.com/css-hover-effects) library.

## Frequently asked questions

### Why doesn't border-radius work with border-image?

By design. A border image can be pushed outside the border box with border-image-outset, so browsers do not clip it to the rounded corner. For a rounded gradient border, paint the gradient as a background clipped to the border box under a solid fill clipped to the padding box, or use a pseudo-element.

### Can you animate a CSS gradient?

Not directly. Gradients are images, and background-image animates discretely, so one gradient snaps to the next. Register a custom property with @property, use it inside the gradient for an angle or stop position, and animate that property instead. Alternatively, move background-position or rotate a pseudo-element with transform.

### How do I make a gradient border with rounded corners?

Give the element a transparent border and two backgrounds: a solid colour clipped to padding-box on top and the gradient clipped to border-box underneath. The gradient shows only in the border ring, which follows border-radius. The inner colour must be opaque.

### Is an animated gradient border bad for performance?

One small one is fine. Animating a @property angle repaints the element every frame on the main thread, so the cost grows with size and count. Rotating a pseudo-element with transform is composited and cheaper. Keep animated borders to a few small elements and honour prefers-reduced-motion.

## Keep going

- [CSS gradient generator](https://frontendhelpers.com/css-gradient-generator): Design the colour stops visually, then copy the gradient into any of these techniques.
 
- [CSS hover effects](https://frontendhelpers.com/css-hover-effects): Copy-paste hover states, including a sliding gradient fill.
 
- [CSS animation performance](https://frontendhelpers.com/css-animation-performance): Which properties repaint, which composite, and how to spot jank.
