22 CSS Gradient Buttons19 / 22

CSS + JSMIT licensed

Hold To Confirm Charging Gradient Button

A destructive-action button that must be pressed and held while a conic gradient ring charges around it — it jitters nervously near completion, fires only at 100%, and drains the charge back down if released early.

Published

Live Demo
Try it

The code

<div class="gx-19">
  <div class="gx-19__card">
    <p class="gx-19__kicker">Danger zone</p>
    <h3 class="gx-19__title">Purge the edge cache</h3>
    <p class="gx-19__copy">This clears every cached asset across all <code>142</code> edge nodes. It cannot be undone — hold the button to confirm.</p>
    <button class="gx-19__btn" type="button" id="gx-19-btn">
      <span class="gx-19__labs">
        <span class="gx-19__lab gx-19__lab--idle">Hold to purge cache</span>
        <span class="gx-19__lab gx-19__lab--done" aria-hidden="true">✓ Cache purged</span>
      </span>
    </button>
    <p class="gx-19__status" id="gx-19-status" aria-live="polite">Hold 1.2 s to confirm</p>
  </div>
</div>
.gx-19,
.gx-19 *,
.gx-19 *::before,
.gx-19 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.gx-19 ::selection {
  background: #a78bfa;
  color: #0b0e1a;
}

@property --gx-19-p {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;
}

.gx-19 {
  --bg: #0b0e1a;
  --panel: #131a2e;
  --line: rgba(148,163,184,.14);
  --c1: #22d3ee;
  --c2: #a78bfa;
  --c3: #f472b6;
  --ink: #e2e8f0;
  --mut: #8b93ab;
  --hold: 1.2s;
  font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
  background: radial-gradient(120% 140% at 82% -20%,#1a2142 0%,var(--bg) 55%);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 48px 24px;
}

.gx-19__card {
  width: min(400px,100%);
  background: linear-gradient(180deg,rgba(255,255,255,.03),rgba(255,255,255,0)) ,var(--panel);
  border: 1px solid var(--line);
  border-radius: 20px;
  padding: 28px 28px 24px;
  box-shadow: 0 30px 60px -30px rgba(0,0,0,.7);
}

.gx-19__kicker {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: #fb7185;
  display: flex;
  align-items: center;
  gap: 8px;
}

.gx-19__kicker::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #fb7185;
  box-shadow: 0 0 10px rgba(251,113,133,.8);
}

.gx-19__title {
  margin-top: 12px;
  font-size: 19px;
  font-weight: 700;
  color: var(--ink);
  letter-spacing: -.01em;
}

.gx-19__copy {
  margin-top: 8px;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--mut);
}

.gx-19__copy code {
  font-family: ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size: 12px;
  background: rgba(148,163,184,.12);
  padding: 1px 6px;
  border-radius: 6px;
  color: #cbd5f5;
}
/* The button. --gx-19-p (0–100) is written every animation frame by JS
   and drives BOTH the conic ring fill and the vivid core opacity. */

.gx-19__btn {
  --gx-19-p: 0;
  position: relative;
  width: 100%;
  margin-top: 22px;
  padding: 16px 24px;
  border: 0;
  border-radius: 999px;
  background: linear-gradient(120deg,#242c4c,#1a2140);
  color: var(--ink);
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: .02em;
  cursor: pointer;
  isolation: isolate;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  box-shadow: 0 0 calc(var(--gx-19-p)*.4px) rgba(167,139,250,calc(var(--gx-19-p)*.006));
}
/* Charging ring: conic gradient hollowed into a ring with a two-layer mask */

.gx-19__btn::before {
  content: "";
  position: absolute;
  inset: -6px;
  border-radius: inherit;
  padding: 3px;
  background: conic-gradient(from -90deg,
      var(--c1) 0%,
      var(--c2) calc(var(--gx-19-p)*.5%),
      var(--c3) calc(var(--gx-19-p)*1%),
      rgba(148,163,184,.16) calc(var(--gx-19-p)*1%));
  -webkit-mask: linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  mask-composite: exclude;
  z-index: -1;
}
/* Vivid core that saturates as the charge rises */

.gx-19__btn::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(120deg,var(--c1),var(--c2) 50%,var(--c3));
  opacity: calc(var(--gx-19-p)/130);
  z-index: -1;
}

.gx-19__btn:focus-visible {
  outline: 2px solid var(--c1);
  outline-offset: 5px;
}
/* Nervous jitter as the charge nears completion */

@keyframes gx-19-jitter {
  0%,
    100% {
    transform: translateX(0);
  }

  25% {
    transform: translateX(-1.5px);
  }

  75% {
    transform: translateX(1.5px);
  }
}

.gx-19__btn.is-arming {
  animation: gx-19-jitter .12s linear infinite;
}
/* Stacked labels crossfade on completion */

.gx-19__labs {
  position: relative;
  display: block;
  pointer-events: none;
}

.gx-19__lab {
  display: block;
  transition: opacity .3s ease,transform .3s ease;
}

.gx-19__lab--done {
  position: absolute;
  inset: 0;
  opacity: 0;
  transform: translateY(6px);
  color: #0b0e1a;
  font-weight: 700;
}

.gx-19__btn.is-done {
  background: linear-gradient(120deg,var(--c1),var(--c2) 50%,var(--c3));
}

.gx-19__btn.is-done .gx-19__lab--idle {
  opacity: 0;
  transform: translateY(-6px);
}

.gx-19__btn.is-done .gx-19__lab--done {
  opacity: 1;
  transform: translateY(0);
}

.gx-19__status {
  margin-top: 14px;
  text-align: center;
  font-size: 12px;
  letter-spacing: .04em;
  color: var(--mut);
  font-variant-numeric: tabular-nums;
  min-height: 1.2em;
}

@media (prefers-reduced-motion: reduce) {
  .gx-19__btn.is-arming {
    animation: none;
  }

  .gx-19__lab {
    transition: none;
  }
}
(function(){
  var btn=document.getElementById('gx-19-btn');
  if(!btn)return;
  var status=document.getElementById('gx-19-status');
  var HOLD=1200;      /* ms to fully charge */
  var DRAIN=2.4;      /* drain speed multiplier when released */
  var p=0,raf=null,last=0,holding=false,done=false;
  function set(v){
    p=Math.max(0,Math.min(100,v));
    btn.style.setProperty('--gx-19-p',p.toFixed(2));
  }
  function frame(t){
    if(!last)last=t;
    var dt=t-last;last=t;
    if(holding){
      set(p+dt/HOLD*100);
      btn.classList.toggle('is-arming',p>78);
      if(p>=100){finish();return}
    }else{
      set(p-dt/HOLD*100*DRAIN);
      btn.classList.remove('is-arming');
      if(p<=0){stopLoop();status.textContent='Hold 1.2 s to confirm';return}
    }
    raf=requestAnimationFrame(frame);
  }
  function startLoop(){if(!raf){last=0;raf=requestAnimationFrame(frame)}}
  function stopLoop(){cancelAnimationFrame(raf);raf=null;last=0}
  function start(){
    if(done)return;
    holding=true;
    status.textContent='Charging\u2026 keep holding';
    startLoop();
  }
  function release(){
    if(done||!holding)return;
    holding=false;
    if(p>0)status.textContent='Released \u2014 charge draining';
    startLoop();
  }
  function finish(){
    done=true;holding=false;stopLoop();set(100);
    btn.classList.remove('is-arming');
    btn.classList.add('is-done');
    status.textContent='Cache purged across 142 nodes.';
    setTimeout(function(){
      done=false;
      btn.classList.remove('is-done');
      set(0);
      status.textContent='Hold 1.2 s to confirm';
    },2600);
  }
  btn.addEventListener('pointerdown',function(e){
    btn.setPointerCapture(e.pointerId);
    start();
  });
  btn.addEventListener('pointerup',release);
  btn.addEventListener('pointercancel',release);
  btn.addEventListener('keydown',function(e){
    if((e.key===' '||e.key==='Enter')&&!e.repeat){e.preventDefault();start()}
  });
  btn.addEventListener('keyup',function(e){
    if(e.key===' '||e.key==='Enter')release();
  });
})();
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 Gradient 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: Hold To Confirm Charging Gradient Button
Source: https://codefronts.com/components/css-gradient-buttons/hold-to-confirm-charging-gradient-button/

A destructive-action button that must be pressed and held while a conic gradient ring charges around it — it jitters nervously near completion, fires only at 100%, and drains the charge back down if released early.
## HTML
```html
<div class="gx-19">
  <div class="gx-19__card">
    <p class="gx-19__kicker">Danger zone</p>
    <h3 class="gx-19__title">Purge the edge cache</h3>
    <p class="gx-19__copy">This clears every cached asset across all <code>142</code> edge nodes. It cannot be undone — hold the button to confirm.</p>
    <button class="gx-19__btn" type="button" id="gx-19-btn">
      <span class="gx-19__labs">
        <span class="gx-19__lab gx-19__lab--idle">Hold to purge cache</span>
        <span class="gx-19__lab gx-19__lab--done" aria-hidden="true">✓ Cache purged</span>
      </span>
    </button>
    <p class="gx-19__status" id="gx-19-status" aria-live="polite">Hold 1.2 s to confirm</p>
  </div>
</div>
```
## CSS
```css
.gx-19,
.gx-19 *,
.gx-19 *::before,
.gx-19 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.gx-19 ::selection {
  background: #a78bfa;
  color: #0b0e1a;
}

@property --gx-19-p {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;
}

.gx-19 {
  --bg: #0b0e1a;
  --panel: #131a2e;
  --line: rgba(148,163,184,.14);
  --c1: #22d3ee;
  --c2: #a78bfa;
  --c3: #f472b6;
  --ink: #e2e8f0;
  --mut: #8b93ab;
  --hold: 1.2s;
  font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
  background: radial-gradient(120% 140% at 82% -20%,#1a2142 0%,var(--bg) 55%);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 48px 24px;
}

.gx-19__card {
  width: min(400px,100%);
  background: linear-gradient(180deg,rgba(255,255,255,.03),rgba(255,255,255,0)) ,var(--panel);
  border: 1px solid var(--line);
  border-radius: 20px;
  padding: 28px 28px 24px;
  box-shadow: 0 30px 60px -30px rgba(0,0,0,.7);
}

.gx-19__kicker {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: #fb7185;
  display: flex;
  align-items: center;
  gap: 8px;
}

.gx-19__kicker::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #fb7185;
  box-shadow: 0 0 10px rgba(251,113,133,.8);
}

.gx-19__title {
  margin-top: 12px;
  font-size: 19px;
  font-weight: 700;
  color: var(--ink);
  letter-spacing: -.01em;
}

.gx-19__copy {
  margin-top: 8px;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--mut);
}

.gx-19__copy code {
  font-family: ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size: 12px;
  background: rgba(148,163,184,.12);
  padding: 1px 6px;
  border-radius: 6px;
  color: #cbd5f5;
}
/* The button. --gx-19-p (0–100) is written every animation frame by JS
   and drives BOTH the conic ring fill and the vivid core opacity. */

.gx-19__btn {
  --gx-19-p: 0;
  position: relative;
  width: 100%;
  margin-top: 22px;
  padding: 16px 24px;
  border: 0;
  border-radius: 999px;
  background: linear-gradient(120deg,#242c4c,#1a2140);
  color: var(--ink);
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: .02em;
  cursor: pointer;
  isolation: isolate;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  box-shadow: 0 0 calc(var(--gx-19-p)*.4px) rgba(167,139,250,calc(var(--gx-19-p)*.006));
}
/* Charging ring: conic gradient hollowed into a ring with a two-layer mask */

.gx-19__btn::before {
  content: "";
  position: absolute;
  inset: -6px;
  border-radius: inherit;
  padding: 3px;
  background: conic-gradient(from -90deg,
      var(--c1) 0%,
      var(--c2) calc(var(--gx-19-p)*.5%),
      var(--c3) calc(var(--gx-19-p)*1%),
      rgba(148,163,184,.16) calc(var(--gx-19-p)*1%));
  -webkit-mask: linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  mask-composite: exclude;
  z-index: -1;
}
/* Vivid core that saturates as the charge rises */

.gx-19__btn::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(120deg,var(--c1),var(--c2) 50%,var(--c3));
  opacity: calc(var(--gx-19-p)/130);
  z-index: -1;
}

.gx-19__btn:focus-visible {
  outline: 2px solid var(--c1);
  outline-offset: 5px;
}
/* Nervous jitter as the charge nears completion */

@keyframes gx-19-jitter {
  0%,
    100% {
    transform: translateX(0);
  }

  25% {
    transform: translateX(-1.5px);
  }

  75% {
    transform: translateX(1.5px);
  }
}

.gx-19__btn.is-arming {
  animation: gx-19-jitter .12s linear infinite;
}
/* Stacked labels crossfade on completion */

.gx-19__labs {
  position: relative;
  display: block;
  pointer-events: none;
}

.gx-19__lab {
  display: block;
  transition: opacity .3s ease,transform .3s ease;
}

.gx-19__lab--done {
  position: absolute;
  inset: 0;
  opacity: 0;
  transform: translateY(6px);
  color: #0b0e1a;
  font-weight: 700;
}

.gx-19__btn.is-done {
  background: linear-gradient(120deg,var(--c1),var(--c2) 50%,var(--c3));
}

.gx-19__btn.is-done .gx-19__lab--idle {
  opacity: 0;
  transform: translateY(-6px);
}

.gx-19__btn.is-done .gx-19__lab--done {
  opacity: 1;
  transform: translateY(0);
}

.gx-19__status {
  margin-top: 14px;
  text-align: center;
  font-size: 12px;
  letter-spacing: .04em;
  color: var(--mut);
  font-variant-numeric: tabular-nums;
  min-height: 1.2em;
}

@media (prefers-reduced-motion: reduce) {
  .gx-19__btn.is-arming {
    animation: none;
  }

  .gx-19__lab {
    transition: none;
  }
}
```

## JavaScript
```js
(function(){
  var btn=document.getElementById('gx-19-btn');
  if(!btn)return;
  var status=document.getElementById('gx-19-status');
  var HOLD=1200;      /* ms to fully charge */
  var DRAIN=2.4;      /* drain speed multiplier when released */
  var p=0,raf=null,last=0,holding=false,done=false;
  function set(v){
    p=Math.max(0,Math.min(100,v));
    btn.style.setProperty('--gx-19-p',p.toFixed(2));
  }
  function frame(t){
    if(!last)last=t;
    var dt=t-last;last=t;
    if(holding){
      set(p+dt/HOLD*100);
      btn.classList.toggle('is-arming',p>78);
      if(p>=100){finish();return}
    }else{
      set(p-dt/HOLD*100*DRAIN);
      btn.classList.remove('is-arming');
      if(p<=0){stopLoop();status.textContent='Hold 1.2 s to confirm';return}
    }
    raf=requestAnimationFrame(frame);
  }
  function startLoop(){if(!raf){last=0;raf=requestAnimationFrame(frame)}}
  function stopLoop(){cancelAnimationFrame(raf);raf=null;last=0}
  function start(){
    if(done)return;
    holding=true;
    status.textContent='Charging\u2026 keep holding';
    startLoop();
  }
  function release(){
    if(done||!holding)return;
    holding=false;
    if(p>0)status.textContent='Released \u2014 charge draining';
    startLoop();
  }
  function finish(){
    done=true;holding=false;stopLoop();set(100);
    btn.classList.remove('is-arming');
    btn.classList.add('is-done');
    status.textContent='Cache purged across 142 nodes.';
    setTimeout(function(){
      done=false;
      btn.classList.remove('is-done');
      set(0);
      status.textContent='Hold 1.2 s to confirm';
    },2600);
  }
  btn.addEventListener('pointerdown',function(e){
    btn.setPointerCapture(e.pointerId);
    start();
  });
  btn.addEventListener('pointerup',release);
  btn.addEventListener('pointercancel',release);
  btn.addEventListener('keydown',function(e){
    if((e.key===' '||e.key==='Enter')&&!e.repeat){e.preventDefault();start()}
  });
  btn.addEventListener('keyup',function(e){
    if(e.key===' '||e.key==='Enter')release();
  });
})();
```

How this works

A single custom property, --gx-19-p (0–100), is written every frame by a requestAnimationFrame loop and drives everything at once: the ring is a ::before painting conic-gradient(from -90deg, ...) whose color stops sit at calc(var(--gx-19-p)*.5%) and calc(var(--gx-19-p)*1%), hollowed into a ring by the two-layer mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0) + mask-composite: exclude trick.

Meanwhile a vivid gradient ::after saturates the core via opacity: calc(var(--gx-19-p)/130), and a hue-matched box-shadow blooms with the same variable. The JS charges over 1.2 s while held, drains 2.4× faster on release, toggles .is-arming (a 1.5px gx-19-jitter shake) past 78%, and locks into .is-done at 100% — with keyboard hold support via Space/Enter keydown/keyup.

Make it yours

  • Change how long the hold takes with the HOLD constant (ms) at the top of the JS; the drain speed is DRAIN — set it to Infinity-like values such as 20 for an instant reset on release.
  • Retint the whole treatment via --c1/--c2/--c3 on .gx-19 — the ring, the saturating core and the glow shadow all read the same trio.
  • Thicken the charging ring by raising the padding: 3px inside ::before, and pull it closer to the button by shrinking inset: -6px.
  • Move the jitter threshold by editing p > 78 in the frame loop — or delete the .is-arming toggle entirely for a calmer charge.
  • Repurpose the pattern for any irreversible action (delete account, send payout, deploy to prod) by swapping the two stacked label spans and the aria-live status strings.

Gotchas — read before shipping

  • The ring mask needs both syntaxes: -webkit-mask-composite: xor for Chrome/Safari and the standard mask-composite: exclude for Firefox — drop one and the ring renders as a filled disc.
  • Call setPointerCapture on pointerdown; without it, dragging a finger slightly off the button never fires pointerup on it and the charge sticks at holding.
  • Don't write the live percentage into the aria-live region every frame — screen readers will queue dozens of announcements. Update it only at state changes (charging, released, done).
  • The trailing hard stop rgba(148,163,184,.16) calc(var(--gx-19-p)*1%) is what keeps the unfilled track visible; remove it and the conic gradient smears across the whole ring.

Browser support

ChromeSafariFirefoxEdge
85+15.4+83+85+

Floor set by conic-gradient plus masked-ring support; the @property registration is a nicety here, not a requirement, since JS writes the value directly.

Search CodeFronts

Loading…