16 CSS Gradient Animations 06 / 16

CSS Animated Gradient Border Card

A pricing card trio where the featured tier sports a razor-thin rotating conic-gradient border driven by the CSS @property Houdini API, creating a continuously spinning rainbow ring that highlights premium tiers.

Pure CSS MIT licensed
Live Demo Open in tab
Open in playground

The code

<div class="ga-06">

  <!-- Free tier — no spinning border -->
  <div class="ga-06__card">
    <div class="ga-06__inner">
      <div class="ga-06__tier">
        <span class="ga-06__tier-name">Starter</span>
        <span class="ga-06__badge ga-06__badge--free">Free</span>
      </div>
      <div class="ga-06__price">
        <span class="ga-06__price-amt">$0</span>
        <span class="ga-06__price-per">/mo</span>
      </div>
      <div class="ga-06__divider"></div>
      <ul class="ga-06__features">
        <li class="ga-06__feature"><span class="ga-06__feature-icon">✓</span>3 projects</li>
        <li class="ga-06__feature"><span class="ga-06__feature-icon">✓</span>5GB storage</li>
        <li class="ga-06__feature"><span class="ga-06__feature-icon">✗</span>Team access</li>
        <li class="ga-06__feature"><span class="ga-06__feature-icon">✗</span>Priority support</li>
      </ul>
      <button class="ga-06__cta ga-06__cta--free">Get Started</button>
    </div>
  </div>

  <!-- Pro tier — always-on spinning gradient border -->
  <div class="ga-06__card ga-06__card--active">
    <div class="ga-06__inner">
      <div class="ga-06__tier">
        <span class="ga-06__tier-name">Pro</span>
        <span class="ga-06__badge ga-06__badge--pro">Most Popular</span>
      </div>
      <div class="ga-06__price">
        <span class="ga-06__price-amt">$29</span>
        <span class="ga-06__price-per">/mo</span>
      </div>
      <div class="ga-06__divider"></div>
      <ul class="ga-06__features">
        <li class="ga-06__feature"><span class="ga-06__feature-icon">✓</span>Unlimited projects</li>
        <li class="ga-06__feature"><span class="ga-06__feature-icon">✓</span>100GB storage</li>
        <li class="ga-06__feature"><span class="ga-06__feature-icon">✓</span>Team access (5 seats)</li>
        <li class="ga-06__feature"><span class="ga-06__feature-icon">✓</span>Priority support</li>
      </ul>
      <button class="ga-06__cta ga-06__cta--pro">Upgrade to Pro</button>
    </div>
  </div>

  <!-- Elite tier — hover reveals border -->
  <div class="ga-06__card">
    <div class="ga-06__inner">
      <div class="ga-06__tier">
        <span class="ga-06__tier-name">Elite</span>
        <span class="ga-06__badge ga-06__badge--elite">Enterprise</span>
      </div>
      <div class="ga-06__price">
        <span class="ga-06__price-amt">$99</span>
        <span class="ga-06__price-per">/mo</span>
      </div>
      <div class="ga-06__divider"></div>
      <ul class="ga-06__features">
        <li class="ga-06__feature"><span class="ga-06__feature-icon">✓</span>Unlimited everything</li>
        <li class="ga-06__feature"><span class="ga-06__feature-icon">✓</span>1TB storage</li>
        <li class="ga-06__feature"><span class="ga-06__feature-icon">✓</span>Unlimited seats</li>
        <li class="ga-06__feature"><span class="ga-06__feature-icon">✓</span>Dedicated SLA</li>
      </ul>
      <button class="ga-06__cta ga-06__cta--elite">Contact Sales</button>
    </div>
  </div>

</div>
.ga-06, .ga-06 *, .ga-06 *::before, .ga-06 *::after {
  margin: 0; padding: 0; box-sizing: border-box;
}
.ga-06 ::selection { background: rgba(168,85,247,.4); color: #fff; }

.ga-06 {
  --bg: #0c0c14;
  --card-bg: #13131f;
  --dur: 3s;
  width: 100%;
  min-height: 100vh;
  background: var(--bg);
  font-family: system-ui, -apple-system, sans-serif;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 24px;
  flex-wrap: wrap;
  padding: 48px 24px;
}

/* ── Rotating gradient border using conic-gradient on ::before ── */
.ga-06__card {
  position: relative;
  width: 240px;
  border-radius: 18px;
  padding: 2px; /* border width */
  background: var(--card-bg);
  isolation: isolate;
}

/* The spinning gradient ring */
.ga-06__card::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1.5px;
  background: conic-gradient(
    from var(--a, 0deg),
    #a855f7, #ec4899, #f97316, #eab308, #22d3ee, #6366f1, #a855f7
  );
  -webkit-mask:
    linear-gradient(#fff 0 0) content-box,
    linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  animation: ga-06-spin var(--dur) linear infinite;
  opacity: 0;
  transition: opacity .4s ease;
}

/* Static subtle border at rest */
.ga-06__card::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1.5px solid rgba(255,255,255,.07);
  pointer-events: none;
}

/* Spin via CSS custom property (Houdini) with JS fallback */
@keyframes ga-06-spin {
  to { --a: 360deg; }
}

/* Always-on card shows border always */
.ga-06__card--active::before { opacity: 1; }

/* Hover reveals border on non-active cards */
.ga-06__card:not(.ga-06__card--active):hover::before { opacity: .7; }

/* Glow behind active card */
.ga-06__card--active {
  filter: drop-shadow(0 0 18px rgba(168,85,247,.25));
}

/* Inner content */
.ga-06__inner {
  background: var(--card-bg);
  border-radius: 16px;
  padding: 28px 24px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.ga-06__tier {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.ga-06__tier-name {
  font-size: .7rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .12em;
  color: rgba(255,255,255,.4);
}
.ga-06__badge {
  font-size: .6rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .09em;
  padding: 3px 9px;
  border-radius: 999px;
}
.ga-06__badge--free   { background: rgba(255,255,255,.07); color: rgba(255,255,255,.4); }
.ga-06__badge--pro    { background: rgba(168,85,247,.15); color: #c084fc; border: 1px solid rgba(168,85,247,.25); }
.ga-06__badge--elite  { background: rgba(234,179,8,.12);  color: #fbbf24; border: 1px solid rgba(234,179,8,.2); }

.ga-06__price {
  display: flex;
  align-items: baseline;
  gap: 3px;
}
.ga-06__price-amt {
  font-size: 2.2rem;
  font-weight: 900;
  color: #fff;
  line-height: 1;
}
.ga-06__price-per {
  font-size: .75rem;
  color: rgba(255,255,255,.35);
}

.ga-06__divider {
  height: 1px;
  background: rgba(255,255,255,.06);
}

.ga-06__features {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.ga-06__feature {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: .8rem;
  color: rgba(255,255,255,.55);
}
.ga-06__feature-icon {
  font-size: .85rem;
  flex-shrink: 0;
}
.ga-06__card--active .ga-06__feature { color: rgba(255,255,255,.72); }

.ga-06__cta {
  margin-top: 4px;
  width: 100%;
  padding: 11px;
  border-radius: 10px;
  border: none;
  font-size: .85rem;
  font-weight: 700;
  cursor: pointer;
  transition: all .25s;
}
.ga-06__cta--free  { background: rgba(255,255,255,.07); color: rgba(255,255,255,.5); }
.ga-06__cta--free:hover { background: rgba(255,255,255,.11); color: rgba(255,255,255,.75); }
.ga-06__cta--pro   { background: linear-gradient(135deg, #7c3aed, #a855f7); color: #fff; }
.ga-06__cta--pro:hover { box-shadow: 0 4px 20px rgba(168,85,247,.4); transform: translateY(-1px); }
.ga-06__cta--elite { background: linear-gradient(135deg, #92400e, #b45309); color: #fef3c7; }
.ga-06__cta--elite:hover { box-shadow: 0 4px 20px rgba(234,179,8,.3); transform: translateY(-1px); }

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

@media (prefers-reduced-motion: reduce) {
  .ga-06__card::before { animation: none; }
}

How this works

The rotating border uses a ::before pseudo-element with background: conic-gradient(from var(--a, 0deg), ...) and then masks out the card interior using the CSS mask compositing trick: -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0) with -webkit-mask-composite: xor (and the standard mask-composite: exclude). This leaves only the narrow padding gap — the border width — visible, because the two mask layers cancel out everywhere except the border ring. The spin itself is driven by @keyframes ga-06-spin { to { --a: 360deg; } } which requires the @property --a Houdini registration so the browser can interpolate angle values.

Cards without the .ga-06__card--active class default to opacity: 0 on the border pseudo-element and reveal it on :hover via a simple opacity transition, making hover-only gradient borders trivial. The inner card content lives inside a .ga-06__inner div with its own background and border-radius, which covers the raw conic-gradient background of the wrapper — the 2px gap between them is the visible animated border.

Customize

  • Widen or narrow the border by adjusting padding: 2px on .ga-06__card — the gap between the wrapper and .ga-06__inner is exactly the border thickness.
  • Change the gradient colour stops in the conic-gradient on .ga-06__card::before — remove stops for a two-colour sweep or add more for a full rainbow ring.
  • Slow the spin by increasing --dur on .ga-06 from 3s to 8s for a subtle premium feel, or speed it to 1s for an energetic tech-startup vibe.
  • Add a glow behind the active card by setting filter: drop-shadow(0 0 24px rgba(168,85,247,.4)) — use drop-shadow rather than box-shadow because box-shadow ignores the border shape.
  • Apply the effect to a non-card element (e.g. an avatar or badge) by swapping the wrapper to a circle with border-radius: 50% and adjusting the inner element accordingly.

Watch out for

  • @property --a (Houdini) is required for the conic-gradient angle to animate smoothly — without it, browsers cannot interpolate angle custom properties and the border will jump rather than spin. Fallback: use a transform: rotate() on the whole pseudo-element instead, accepting the gradient directions will rotate with it.
  • The mask composite trick (-webkit-mask-composite: xor vs mask-composite: exclude) uses different keyword syntax in WebKit vs the standard — always include both to support Safari and Chrome simultaneously.
  • Setting overflow: hidden on the parent .ga-06 wrapper will clip the drop-shadow glow on .ga-06__card--active — use overflow: visible on the wrapper and clip only where specifically needed.

Browser support

ChromeSafariFirefoxEdge
111+ 16.4+ 128+ 111+

@property (Houdini) for smooth angle interpolation requires Chrome 85+, Safari 16.4+, Firefox 128+. Without it, use a transform: rotate() fallback — the conic-gradient renders everywhere but will not animate in older browsers.

Search CodeFronts

Loading…