51 CSS Buttons05 / 51

Pure CSSMIT licensed

CSS Gradient CTA Button

A conversion-grade CTA with a conic gradient ring that rotates continuously, a soft bloom underneath, and a mesh gradient body that shifts on hover. The rotation is possible at all because @property registers the angle as an <angle> type — plain custom properties cannot be animated.

Published

Live Demo
Try it

The code

<div class="cb-05">
  <div class="cb-05__stage">
    <button class="cb-05__btn" type="button">
      <svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M13 3 4.5 13.5H11l-1 7.5 8.5-10.5H12l1-7.5z"/></svg>
      <span>Upgrade to Pro</span>
    </button>
  </div>
</div>
@import url('https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&display=swap');

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

.cb-05 {
  --bg: #08070d;
  --c1: #6d28d9;
  --c2: #2dd4bf;
  --c3: #f472b6;
  --ink: #ffffff;
  --body: #141024;
  --bloom: .55;
  --radius: 1rem;
  --sans: "Sora",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: radial-gradient(60rem 40rem at 50% 120%, color-mix(in oklab,var(--c1) 22%,transparent), transparent 70%), var(--bg);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
}

@supports (color: oklch(50% 0 0)) {
  .cb-05 {
    --c1: oklch(52% .24 293);
    --c2: oklch(80% .14 182);
    --c3: oklch(74% .17 349);
    --body: oklch(20% .04 290);
  }
}

@media (prefers-color-scheme: light) {
  .cb-05:not([data-theme="dark"]) {
    --bg: #f5f4fb;
    --body: #1a1530;
    --bloom: .4;
  }
}

.cb-05[data-theme="light"] {
  --bg: #f5f4fb;
  --body: #1a1530;
  --bloom: .4;
}

.cb-05,
.cb-05 *,
.cb-05 *::before,
.cb-05 *::after {
  box-sizing: border-box;
}

.cb-05__stage {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: clamp(1.5rem,6vw,4rem);
}

.cb-05__btn {
  position: relative;
  isolation: isolate;
  cursor: pointer;
  display: grid;
  grid-auto-flow: column;
  align-items: center;
  gap: .55rem;
  min-height: 3.5rem;
  padding: 0 1.9rem;
  border: 0;
  border-radius: var(--radius);
  font: inherit;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: -.005em;
  color: var(--ink);
  background: radial-gradient(120% 140% at 15% 0%, color-mix(in oklab,var(--c1) 55%,transparent), transparent 60%), radial-gradient(120% 140% at 90% 100%, color-mix(in oklab,var(--c2) 35%,transparent), transparent 60%), var(--body);
  background-size: 150% 150%,150% 150%,100% 100%;
  background-position: 0% 0%,100% 100%,0 0;
  transition: translate .3s cubic-bezier(.16,1,.3,1), background-position .6s cubic-bezier(.16,1,.3,1), scale .18s ease;
}

.cb-05__btn svg {
  width: 1.1rem;
  height: 1.1rem;
  color: var(--c2);
}

.cb-05__btn::before,
.cb-05__btn::after {
  content: '';
  position: absolute;
  inset: -2px;
  z-index: -1;
  border-radius: inherit;
  background: conic-gradient(from var(--cb-05-a),var(--c1),var(--c2),var(--c3),var(--c1));
  animation: cb-05-spin 4.5s linear infinite;
}

.cb-05__btn::after {
  inset: 2px;
  filter: blur(20px);
  opacity: var(--bloom);
  transition: opacity .35s ease;
}

@keyframes cb-05-spin {
  to {
    --cb-05-a: 495deg;
  }
}

.cb-05__btn:hover {
  translate: 0 -2px;
  background-position: 30% 20%,70% 80%,0 0;
}

.cb-05__btn:hover::after {
  opacity: calc(var(--bloom) + .35);
}

.cb-05__btn:active {
  scale: .98;
  translate: 0 0;
}

.cb-05__btn:focus-visible {
  outline: 2px solid var(--c2);
  outline-offset: 5px;
}

.cb-05__btn:disabled {
  cursor: not-allowed;
  filter: grayscale(.85);
  opacity: .55;
}

.cb-05__btn:disabled::before,
.cb-05__btn:disabled::after {
  animation-play-state: paused;
}

@media (prefers-reduced-motion: reduce) {
  .cb-05__btn::before,
    .cb-05__btn::after {
    animation: none;
  }

  .cb-05__btn {
    transition: none;
  }
}

@media (forced-colors: active) {
  .cb-05__btn {
    background: ButtonFace;
    color: ButtonText;
    border: 1px solid ButtonText;
  }

  .cb-05__btn::before,
    .cb-05__btn::after {
    display: none;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Button from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Gradient CTA Button
Source: https://codefronts.com/components/css-buttons/css-gradient-cta-button/

A conversion-grade CTA with a conic gradient ring that rotates continuously, a soft bloom underneath, and a mesh gradient body that shifts on hover. The rotation is possible at all because @property registers the angle as an <angle> type — plain custom properties cannot be animated.
## HTML
```html
<div class="cb-05">
  <div class="cb-05__stage">
    <button class="cb-05__btn" type="button">
      <svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M13 3 4.5 13.5H11l-1 7.5 8.5-10.5H12l1-7.5z"/></svg>
      <span>Upgrade to Pro</span>
    </button>
  </div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&display=swap');

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

.cb-05 {
  --bg: #08070d;
  --c1: #6d28d9;
  --c2: #2dd4bf;
  --c3: #f472b6;
  --ink: #ffffff;
  --body: #141024;
  --bloom: .55;
  --radius: 1rem;
  --sans: "Sora",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: radial-gradient(60rem 40rem at 50% 120%, color-mix(in oklab,var(--c1) 22%,transparent), transparent 70%), var(--bg);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
}

@supports (color: oklch(50% 0 0)) {
  .cb-05 {
    --c1: oklch(52% .24 293);
    --c2: oklch(80% .14 182);
    --c3: oklch(74% .17 349);
    --body: oklch(20% .04 290);
  }
}

@media (prefers-color-scheme: light) {
  .cb-05:not([data-theme="dark"]) {
    --bg: #f5f4fb;
    --body: #1a1530;
    --bloom: .4;
  }
}

.cb-05[data-theme="light"] {
  --bg: #f5f4fb;
  --body: #1a1530;
  --bloom: .4;
}

.cb-05,
.cb-05 *,
.cb-05 *::before,
.cb-05 *::after {
  box-sizing: border-box;
}

.cb-05__stage {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: clamp(1.5rem,6vw,4rem);
}

.cb-05__btn {
  position: relative;
  isolation: isolate;
  cursor: pointer;
  display: grid;
  grid-auto-flow: column;
  align-items: center;
  gap: .55rem;
  min-height: 3.5rem;
  padding: 0 1.9rem;
  border: 0;
  border-radius: var(--radius);
  font: inherit;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: -.005em;
  color: var(--ink);
  background: radial-gradient(120% 140% at 15% 0%, color-mix(in oklab,var(--c1) 55%,transparent), transparent 60%), radial-gradient(120% 140% at 90% 100%, color-mix(in oklab,var(--c2) 35%,transparent), transparent 60%), var(--body);
  background-size: 150% 150%,150% 150%,100% 100%;
  background-position: 0% 0%,100% 100%,0 0;
  transition: translate .3s cubic-bezier(.16,1,.3,1), background-position .6s cubic-bezier(.16,1,.3,1), scale .18s ease;
}

.cb-05__btn svg {
  width: 1.1rem;
  height: 1.1rem;
  color: var(--c2);
}

.cb-05__btn::before,
.cb-05__btn::after {
  content: '';
  position: absolute;
  inset: -2px;
  z-index: -1;
  border-radius: inherit;
  background: conic-gradient(from var(--cb-05-a),var(--c1),var(--c2),var(--c3),var(--c1));
  animation: cb-05-spin 4.5s linear infinite;
}

.cb-05__btn::after {
  inset: 2px;
  filter: blur(20px);
  opacity: var(--bloom);
  transition: opacity .35s ease;
}

@keyframes cb-05-spin {
  to {
    --cb-05-a: 495deg;
  }
}

.cb-05__btn:hover {
  translate: 0 -2px;
  background-position: 30% 20%,70% 80%,0 0;
}

.cb-05__btn:hover::after {
  opacity: calc(var(--bloom) + .35);
}

.cb-05__btn:active {
  scale: .98;
  translate: 0 0;
}

.cb-05__btn:focus-visible {
  outline: 2px solid var(--c2);
  outline-offset: 5px;
}

.cb-05__btn:disabled {
  cursor: not-allowed;
  filter: grayscale(.85);
  opacity: .55;
}

.cb-05__btn:disabled::before,
.cb-05__btn:disabled::after {
  animation-play-state: paused;
}

@media (prefers-reduced-motion: reduce) {
  .cb-05__btn::before,
    .cb-05__btn::after {
    animation: none;
  }

  .cb-05__btn {
    transition: none;
  }
}

@media (forced-colors: active) {
  .cb-05__btn {
    background: ButtonFace;
    color: ButtonText;
    border: 1px solid ButtonText;
  }

  .cb-05__btn::before,
    .cb-05__btn::after {
    display: none;
  }
}
```

How this works

Registering the property gives the browser a type it knows how to interpolate, which turns an un-animatable gradient into a smooth one:

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

.cb-05__btn::before{
  content:''; position:absolute; inset:-2px; border-radius:inherit; z-index:-1;
  background:conic-gradient(from var(--cb-05-a),
    var(--c1),var(--c2),var(--c3),var(--c1));
  animation:cb-05-spin 4.5s linear infinite;
}
@keyframes cb-05-spin{ to{ --cb-05-a:360deg; } }

The ring is a slightly larger layer behind the button, so the button's own opaque background masks the middle and only a 2px rim shows — cheaper and rounder than a border-image. A second copy of the same layer, blurred with filter:blur(18px) and opacity, is the bloom; on hover the opacity rises and the button lifts.

The body itself is two overlapping radial gradients whose background-position shifts on hover, so the surface feels lit rather than flat.

Make it yours

  • Recolour with the three stop tokens --c1/--c2/--c3 — keep them within ~120° of hue for a premium look, spread them wide for a playful one.
  • Speed: 4.5s reads as premium; under 2s it looks like a loading state, which is a usability bug on a CTA.
  • Thicken the ring by changing inset:-2px to -3px; the radius follows automatically via border-radius:inherit.
  • Turn the bloom off for dark UI density by setting --bloom:0; hover still raises it.
  • For a static gradient (no motion budget) delete the animation and keep from 135deg — the ring still reads as premium.

Gotchas — read before shipping

  • A custom property is a token string until you register it; @property with the right syntax is mandatory or the keyframes simply snap at 50%.
  • Infinite motion on a primary CTA is an accessibility risk — the prefers-reduced-motion block here stops the spin and keeps a fixed-angle gradient, which is not optional.
  • filter:blur() on a large layer is fill-rate heavy; keep the bloom element at the button's size, never full-viewport, and never animate the blur radius itself.
  • Gradient text on gradient fills destroys contrast. Keep the label a solid colour with ≥4.5:1 against the darkest gradient stop.
  • Put the ring at z-index:-1 with isolation:isolate on the button — without isolation the negative index can slip behind the page background in some stacking contexts.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Floor set by oklch(), color-mix(), @property as this demo is written. The core technique itself goes back further — Chrome 85+.

@property needs Chrome 85 / Safari 16.4 / Firefox 128; older engines show the ring at its initial angle without spinning (fully usable). conic-gradient is universal since 2020.

Techniques used in this demo

Search CodeFronts

Loading…