EmpireUI
Get Pro
← Blog8 min read#gradient border#animation#css

Animated Gradient Border in CSS: Spinning Rainbow Border on Any Element

Learn how to build a spinning animated gradient border in pure CSS using conic-gradient and @keyframes — works on buttons, cards, and any HTML element.

colorful spinning rainbow gradient border glowing around a dark card element

Why Animated Gradient Borders Are Everywhere Right Now

You've seen this effect on every hot SaaS landing page in 2025 and 2026 — a card, a button, or an input with a rainbow border that slowly spins around the perimeter. It looks expensive. It's actually like 30 lines of CSS. That gap between perceived complexity and actual effort is exactly why the effect is so popular right now.

The trick is that CSS doesn't have a border-image-animation property. You can't just throw a gradient into border and call it done. border-image exists, but it breaks border-radius, which kills the effect immediately. So the whole approach is a workaround — a clever one — using a pseudo-element and conic-gradient that rotates underneath the real element via @keyframes.

Honestly, once you understand the stacking model here, you'll start seeing opportunities to use it everywhere. Highlighted components, premium plan cards, active states, focus rings on inputs. The technique scales. And if you want a head start with polished implementations, the Empire UI component library ships several gradient-border variants you can copy in one click.

That said, let's build it from scratch so you actually understand what's happening and why each property matters.

The Core Technique: Pseudo-Element + conic-gradient + @keyframes

Here's the mental model. Your element sits on top. Behind it — via a ::before pseudo-element set to z-index: -1 — you put a box that's 4px larger on every side. That box gets the conic-gradient spinning animation. Because it bleeds out from under the element by exactly 4px, those 4px look like a border. Your actual element clips everything in the center with a matching border-radius and a solid background.

The conic-gradient is key. Unlike linear-gradient or radial-gradient, conic-gradient sweeps colors around a center point — which is perfect for a border that needs to travel around the perimeter. Rotating it with @keyframes spins that color sweep continuously.

.gradient-border {
  position: relative;
  border-radius: 12px;
  background: #0f0f0f;
  padding: 24px 32px;
  /* Create stacking context so ::before can go behind */
  isolation: isolate;
}

.gradient-border::before {
  content: '';
  position: absolute;
  inset: -3px; /* bleeds 3px on all sides = border width */
  border-radius: 14px; /* 12px + 2px to cover the inset */
  background: conic-gradient(
    from 0deg,
    #ff0080,
    #ff8c00,
    #ffd700,
    #7dff6b,
    #00cfff,
    #a855f7,
    #ff0080
  );
  z-index: -1;
  animation: spin-border 3s linear infinite;
}

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

Notice the inset: -3px — that's your "border width". Change it to -1px for a hairline border or -6px for something chunkier. The border-radius on ::before needs to be 2px larger than the element's own radius to cover the corners cleanly. Quick aside: isolation: isolate on the parent is non-negotiable — without it, z-index: -1 on the pseudo-element can slip behind the page background entirely.

Worth noting: rotate: 360deg in @keyframes uses the newer individual transform property (CSS Transforms Level 2), supported in all major browsers since 2022. If you need to support Chrome 103 or earlier, fall back to transform: rotate(360deg) instead.

Making It Work with Border-Radius and Overflow

The most common bug people hit: the spinning gradient bleeds outside the rounded corners and looks wrong. It happens because the pseudo-element rotates around its own center — and when conic-gradient sweeps to a corner position, some color can poke out past the curve you expect.

The fix is overflow: hidden on the parent combined with a slight radius adjustment. But there's a catch — overflow: hidden clips box-shadow and anything else that should legitimately escape the bounds. If you need a drop shadow on your element, switch to clip-path instead. For most cards and buttons, overflow: hidden is fine.

/* Safe version with overflow: hidden */
.gradient-border {
  position: relative;
  border-radius: 12px;
  background: #0f0f0f;
  overflow: hidden; /* clips the rotating pseudo-element */
  isolation: isolate;
  padding: 24px 32px;
}

.gradient-border::before {
  content: '';
  position: absolute;
  /* Make it big enough to fill even while rotating */
  inset: -100%;
  background: conic-gradient(
    from 0deg,
    #ff0080,
    #ff8c00,
    #ffd700,
    #7dff6b,
    #00cfff,
    #a855f7,
    #ff0080
  );
  animation: spin-border 3s linear infinite;
  z-index: -1;
}

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

With inset: -100% the pseudo-element covers a massive area — that means the gradient always covers the element regardless of rotation angle. overflow: hidden on the parent clips it back down. This is the cleaner approach because you stop chasing the "right" inset value as elements resize.

One more thing — if you're building this in React with Tailwind, you'll hit a wall because Tailwind doesn't have conic-gradient utilities out of the box (as of Tailwind v4). You'll need either a custom plugin or style prop injection. There's an example of that in the React section below.

React + Tailwind Implementation

Tailwind handles layout and spacing cleanly. The gradient animation part lives in a CSS file or a <style> tag. Here's a component that wraps any content with an animated gradient border and accepts a speed and colors prop so you're not stuck with the same rainbow.

// components/GradientBorder.tsx
import { CSSProperties, ReactNode } from 'react';
import './gradient-border.css'; // see CSS below

interface GradientBorderProps {
  children: ReactNode;
  colors?: string[];
  speed?: number; // seconds per rotation
  width?: number; // border width in px
  radius?: number; // border radius in px
  className?: string;
}

export function GradientBorder({
  children,
  colors = ['#ff0080', '#ff8c00', '#ffd700', '#00cfff', '#a855f7', '#ff0080'],
  speed = 3,
  width = 3,
  radius = 12,
  className = '',
}: GradientBorderProps) {
  const gradient = `conic-gradient(from 0deg, ${colors.join(', ')})`;

  return (
    <div
      className={`gradient-border-root ${className}`}
      style={{
        '--gb-radius': `${radius}px`,
        '--gb-width': `${width}px`,
        '--gb-speed': `${speed}s`,
        '--gb-gradient': gradient,
      } as CSSProperties}
    >
      <div className="gradient-border-inner">{children}</div>
    </div>
  );
}
/* gradient-border.css */
.gradient-border-root {
  position: relative;
  display: inline-block;
  border-radius: var(--gb-radius);
  overflow: hidden;
  isolation: isolate;
  padding: var(--gb-width); /* this IS the border */
}

.gradient-border-root::before {
  content: '';
  position: absolute;
  inset: -100%;
  background: var(--gb-gradient);
  animation: gb-spin var(--gb-speed) linear infinite;
  z-index: -1;
}

.gradient-border-inner {
  position: relative;
  border-radius: calc(var(--gb-radius) - var(--gb-width));
  background: #0f0f0f; /* match your page background */
  height: 100%;
}

@keyframes gb-spin {
  to { rotate: 360deg; }
}

The padding: var(--gb-width) trick on the root element means the inner div sits inset by exactly the border width — no absolute positioning games inside the content area. Clean. You can swap background: #0f0f0f for any color, or use background: transparent if you want the gradient to show through the content area too (useful for inputs).

In practice, the CSS custom property approach (--gb-radius, --gb-gradient) is much more reusable than hardcoding values. Pass different color arrays from different call sites and you get completely different vibes — a subtle two-tone blue for a Pro plan card, a full neon rainbow for a CTA button — all from one component.

If you want pre-built gradient combinations without the guesswork, the gradient generator on Empire UI lets you visually build conic gradients and copy the CSS output. Pair it with the component above and you're done.

Animating Only Part of the Border (Arc Effect)

The spinning rainbow is dramatic. Sometimes you want something quieter — a glowing arc that sweeps around the perimeter instead of a full color wheel. Think of it as a progress ring without the progress. This is the effect you see on premium dark-mode cards where a single bright point of light travels around the edge.

/* Arc / spotlight border */
.arc-border::before {
  background: conic-gradient(
    from 0deg,
    transparent 0deg,
    transparent 270deg,
    #00cfff 300deg,
    #ffffff 360deg
  );
  animation: spin-border 4s linear infinite;
}

The transparent stops push color out of most of the circle. Only the last 90deg (from 270 to 360) actually renders the bright arc. Adjust those stop positions to make the arc longer or shorter. A 30deg arc looks like a sharp glint; a 180deg arc looks like a soft glow sweep.

Look, this variant pairs extremely well with dark glassmorphism cards. The glassmorphism components on Empire UI use a similar trick — a subtle arc border combined with backdrop-filter: blur(16px) gives you that premium "hovering" quality without being garish about it.

Worth noting: both the full rainbow and the arc approach are pure CSS with zero JavaScript. Animation performance is handled by the compositor thread (GPU), so you're not blocking the main thread. On mid-range Android phones from 2023 you'll still see 60fps with 3-4 of these on screen simultaneously.

Pausing on Hover and Respecting prefers-reduced-motion

Spinning indefinitely is fine for a hero section. It's annoying on a card that lives in a scrollable feed. The quick fix: pause the animation on hover so users can read the content without a spinning distraction.

.gradient-border-root:hover::before {
  animation-play-state: paused;
}

/* Respect user motion preferences */
@media (prefers-reduced-motion: reduce) {
  .gradient-border-root::before {
    animation: none;
    /* Static gradient instead — still looks great */
    background: conic-gradient(
      from 45deg,
      #ff0080,
      #ff8c00,
      #ffd700,
      #00cfff,
      #a855f7,
      #ff0080
    );
  }
}

The static conic-gradient fallback is important. Without it, users with prefers-reduced-motion: reduce get a plain invisible border — the whole point of the component disappears. A frozen gradient at 45deg still looks polished, it just doesn't spin.

In React, you can also wire this to a context. If your app has an accessibility settings panel where users toggle motion, read from that context in your component and conditionally remove the animation class. Pairing this with the prefers-reduced-motion media query gives you belt-and-suspenders coverage across both OS-level and app-level preferences.

Performance Tips and When to Use This Effect

The rotating pseudo-element triggers GPU compositing automatically because rotate and transform are composited properties. You won't see will-change: transform doing anything extra here — browsers are smart enough by 2026 to composite @keyframes animations on transform properties without hints. Save will-change for scroll-driven animations where it still matters.

Don't stack more than 6-8 of these spinning borders on a single viewport. Each one is a separate compositing layer. On lower-end devices — Snapdragon 680 class — that many layers competing simultaneously will drop frames during scroll. If you have a grid of cards and want the effect, consider triggering the animation only on the hovered card using animation-play-state: running and defaulting to paused.

Where does this effect shine? CTA buttons where click rate matters. Highlighted pricing tiers. Active states on tabs or nav items. Input focus rings on dark forms. It's a terrible idea for pagination, data tables, or anything users need to scan quickly — the motion competes with the content. Reserve it for elements you want users to notice. Check out the box shadow generator too — combining a glowing box-shadow with a gradient border stacks these effects nicely and produces that holographic card look that dominated design Twitter in late 2025.

One final thing: if you want all of this pre-built with multiple variants, color themes, and React components you can install via CLI, browse Empire UI. The gradient border component ships with the aurora, cyberpunk, and vaporwave style tokens — different color palettes, same underlying technique.

FAQ

Why can't I just use border-image with a gradient?

border-image works for static gradients but breaks border-radius entirely — you get sharp corners regardless of what you set. The pseudo-element technique is the only way to get both an animated gradient border and rounded corners in CSS today.

How do I control the border width with this technique?

Use inset: -Npx on the pseudo-element where N is your desired border width in pixels, or use padding: Npx on the wrapper approach. Both control how many pixels of the spinning gradient are visible around the element's edge.

Does the animated gradient border work on Safari?

Yes — Safari 15.4+ supports conic-gradient fully, and rotate in @keyframes works in Safari 16+. If you need to target older Safari, fall back to transform: rotate(360deg) in your keyframes instead.

Can I use this effect on an input or textarea?

You can, but form elements can't have pseudo-elements directly. Wrap the input in a div and apply the gradient border technique to the wrapper, then remove the wrapper's default background so the input fills it. Set border: none on the input itself.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Card Hover Effects in CSS: 12 Patterns From Subtle to DramaticHolographic Effect in CSS: Rainbow Foil Cards and BadgesCSS Gradient Animation: background-size, background-position TricksCSS Hover Effects Gallery: 10 Patterns Beyond color and opacity