Buttons✨ Premium

Gradient Border Animated

A 2 px ring in a pink → orange → turquoise conic gradient whose start angle is animated from 0 to 360° in 3 s. One often-forgotten condition: the @property declaration that makes the --angle variable animatable — without it, the ring stands still.

CSSAnimationBorder

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Buttons has 27 effects, including 2 free. Explore the category →

3 usage examples

Generate an image — creative AI tool landing page — Gradient Border Animated example 1

① Generate an image — creative AI tool landing page

WhenDark hero of an image generator: headline, sample prompt, one action button.
WhyThe pink → orange → turquoise trio is a “creative” palette that stands apart from SaaS indigos; the continuous spinning ring evokes processing in progress, fitting for a tool that “makes”.
Settings16 px text, padding: 18px 40px, 4s turn, @property --angle declared once for the whole page.
Follow — creator profile card — Gradient Border Animated example 2

② Follow — creator profile card

WhenProfile card: avatar, name, stats, one follow button.
WhyOn a dark card, the multicolor ring around a dark button borrows the visual code of “stories”: colorful, animated, without filling the button. One ring per card to stay readable.
SettingsCompact button (padding: 10px 22px, 13.5 px), border-radius: 999px on all three layers for a pill, 3s turn.
Choose a file — drop zone of an import form — Gradient Border Animated example 3

③ Choose a file — drop zone of an import form

WhenDrag-and-drop zone: icon, instruction, accepted formats and a browse button.
WhyThe spinning ring flags the card's active zone without blinking text; at 2 px it stays sober in a working interface. The gradient acts as a “ready to receive” indicator.
Settingspadding: 11px 20px button, 13.5 px, 1.5px ring (::after { inset: 1.5px }), 5s turn, #12121a face to stand out from the zone.

How it works

Three layers, like Rotating Border: the .btn-gradient-border-anim button (background: #0a0a0f, position: relative; z-index: 1, overflow: hidden; border-radius: 12px via .demo-btn), a ::before at z-index: -2 carrying the gradient (inset: -3px; border-radius: 15px) and an ::after at z-index: -1 redrawing the face (inset: 2px; background: #0a0a0f; border-radius: 10px). The 2 px gap between the button's edge and the face is the visible ring.

The difference from Rotating Border lies in what rotates. Here the box doesn't move: the gradient is conic-gradient(from var(--angle, 0deg), #ff0080, #ff8c00, #40e0d0, #ff0080) and the rotateGradientAnim animation (3s linear infinite) varies --angle from 0deg to 360deg. Since the ::before always covers the whole button, the ring should stay continuous at every instant — that's the right principle, the one that avoids gaps on the flanks.

But a custom property is only interpolable if it is registered with @property. Without that declaration, --angle is a string to the browser, animated discretely — 0deg for the first half of the cycle, 360deg for the second. And 0° and 360° draw the same gradient. Measured in Chromium with the CSS alone: --angle is 0deg at 0, 375, 750 and 1,125 ms, 360deg at 1,500 ms, and the ring's pixels are identical at all those instants — the button is static. That is the symptom of a forgotten @property.

The essential rule — @property --angle { syntax: '<angle>'; inherits: false; initial-value: 0deg }, once in the stylesheet — changes everything: the angle passes through 45° at 375 ms and 90° at 750 ms, the colors travel along the ring and the gradient makes a full turn every 3 s without ever breaking. Make sure it travels with the CSS you paste. No :hover or :active rule: the animation runs permanently.

Accessibility

  • Text contrast: white on #0a0a0f = 19.8:1, AAA. The face is opaque: the page color behind it changes nothing.
  • Ring contrast (WCAG 1.4.11, 3:1 threshold for a component's boundary): on #0a0a0f, #ff0080 = 5.2:1, #ff8c00 = 8.5:1, #40e0d0 = 12:1 — the 2 px outline is perceptible at every point of the cycle, and it is continuous, unlike Rotating Border.
  • prefers-reduced-motion not handled: the gradient spins permanently. Add @media (prefers-reduced-motion: reduce) { .btn-gradient-border-anim::before { animation: none } } — the ring remains, frozen at 0°, complete and readable.
  • Screen readers: both pseudo-elements have content: "", nothing is announced, the accessible name is the label. The button remains a native <button>; the animation doesn't depend on hover.
  • Focus: outline is untouched, the native focus ring stays visible and is not clipped by overflow: hidden. Target ≈ 54 px tall (18 px padding + text), above the 44 px minimum.

Browser compatibility

Requires @property, conic-gradient() with from var(), the inset shorthand and the animation of a custom property. No JavaScript, zero dependencies.

Chrome 87+✓ Full
Firefox 128+✓ Full
Safari 16.4+✓ Full
Edge 87+✓ Full
Mobile iOS 16.4+✓ Full
Android Chrome✓ Full

Without @property — Firefox before 128, Safari before 16.4, or if the rule was left out — the gradient ring is present but motionless: a button with a fixed conic border, readable and complete. Without inset (Safari before 14.1, Firefox before 66), the pseudo-elements have neither size nor position and only the white text on the face remains.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
@property --angle (CSS — essential, once per stylesheet) angle type, initial value 0deg @property --angle { syntax: '<angle>'; inherits: false; initial-value: 0deg } makes the variable interpolable. Without it, no rotation. Chrome 85+, Safari 16.4+, Firefox 128+.
animation-duration (CSS — ::before, rotateGradientAnim) 3s Length of a full turn. 2s = brisk; 8s = slow, almost a breath. The rotation is continuous, unlike Rotating Border.
conic-gradient(from var(--angle), #ff0080, #ff8c00, #40e0d0, #ff0080) (CSS — ::before) pink → orange → turquoise → pink Ring colors. Repeat the first one last to avoid a hard seam. Two colors are enough for a spinning two-tone ring.
inset (CSS — ::after) 2px Ring thickness: the gap between the button's edge and the face. 1px = hairline; 3px = frame. Adjust the face's border-radius (12 − inset).
background (CSS — ::after) #0a0a0f Face color. Same as the catalog's background; on another page, match your surface or lighten it for a clean edge.
border-radius (CSS — .demo-btn 12px / ::before 15px / ::after 10px) 12 / 15 / 10 px Rounding of the three layers. Face = button − face inset. 999px everywhere = pill; the ring stays continuous, since the box doesn't rotate.
animation-play-state (CSS — to add) running paused at rest and running under :hover::before for a still ring that starts spinning on hover, without restarting from zero.

FAQ

The @property declaration, almost always. The code animates --angle from 0 to 360°, but an unregistered custom property has no type: the browser can't compute 45° in between and flips from one value to the other halfway — and 0° and 360° give the same gradient. Check that @property --angle { syntax: '<angle>'; inherits: false; initial-value: 0deg } sits at the top of your CSS. The rotation starts immediately, with no other change. If the rule is there and nothing moves, your browser doesn't know @property (next question).
A fixed gradient ring: they don't know @property, so --angle stays discrete and 0° = 360°. The button is complete and readable, only the rotation is missing — an acceptable fallback. If rotation is essential on those browsers, fall back on Rotating Border's technique (spinning the ::before's box with transform), making the ::before a square larger than the diagonal so the ring stays continuous.
Because a spinning rectangle no longer covers the flanks of a wide button as soon as it leaves 0° or 180°: the ring breaks there. Animating the gradient's angle leaves the box in place and the ring intact at every instant — the right solution, at the cost of @property. The universal compromise is a square ::before (width: 200%; aspect-ratio: 1, centered) spun by transform: continuous everywhere, but heavier to composite than animating a variable.