22 CSS Gradient Buttons22 / 22
Cursor Tracking Aurora Gradient Button
A dark glass button whose aurora core and gradient border spotlight both chase the pointer with a lazy trailing glide, then drift back to center when the cursor leaves — powered by transitions on registered @property percentages.
Published
The code
<div class="gx-22">
<div class="gx-22__card">
<span class="gx-22__badge">Private beta</span>
<h3 class="gx-22__title">Lumen is almost ready</h3>
<p class="gx-22__copy">A calmer way to read the internet. We let a few hundred people in every Friday.</p>
<button class="gx-22__btn" type="button" id="gx-22-btn">
<span class="gx-22__spark" aria-hidden="true">✦</span>
Request an invite
</button>
<p class="gx-22__fine">No card required · 2,418 people ahead of you</p>
</div>
</div><div class="gx-22">
<div class="gx-22__card">
<span class="gx-22__badge">Private beta</span>
<h3 class="gx-22__title">Lumen is almost ready</h3>
<p class="gx-22__copy">A calmer way to read the internet. We let a few hundred people in every Friday.</p>
<button class="gx-22__btn" type="button" id="gx-22-btn">
<span class="gx-22__spark" aria-hidden="true">✦</span>
Request an invite
</button>
<p class="gx-22__fine">No card required · 2,418 people ahead of you</p>
</div>
</div>.gx-22,
.gx-22 *,
.gx-22 *::before,
.gx-22 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.gx-22 ::selection {
background: #67e8f9;
color: #0a0d1c;
}
@property --gx-22-x {
syntax: '<percentage>';
inherits: true;
initial-value: 50%;
}
@property --gx-22-y {
syntax: '<percentage>';
inherits: true;
initial-value: 50%;
}
.gx-22 {
--bg: #0a0d1c;
--ink: #e6e9f5;
--mut: #8a91ad;
--a1: #67e8f9;
--a2: #a78bfa;
--a3: #f0abfc;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
background: radial-gradient(90% 70% at 15% 0%,rgba(103,232,249,.06),transparent 55%), radial-gradient(80% 60% at 90% 100%,rgba(167,139,250,.08),transparent 55%), var(--bg);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 48px 24px;
}
.gx-22__card {
width: min(400px,100%);
text-align: center;
padding: 36px 32px;
border-radius: 24px;
background: rgba(255,255,255,.025);
border: 1px solid rgba(148,163,184,.12);
box-shadow: 0 40px 80px -40px rgba(0,0,0,.8);
}
.gx-22__badge {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 5px 12px;
border-radius: 999px;
border: 1px solid rgba(103,232,249,.25);
font-size: 11px;
font-weight: 600;
letter-spacing: .1em;
text-transform: uppercase;
color: var(--a1);
}
.gx-22__badge::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--a1);
box-shadow: 0 0 8px var(--a1);
}
.gx-22__title {
margin-top: 18px;
font-size: 24px;
font-weight: 700;
letter-spacing: -.02em;
color: var(--ink);
}
.gx-22__copy {
margin: 10px auto 0;
max-width: 32ch;
font-size: 14px;
line-height: 1.6;
color: var(--mut);
}
/* The button. --gx-22-x / --gx-22-y are registered <percentage> custom
properties, so they INTERPOLATE: JS writes raw pointer coords, CSS
transitions them, giving the aurora its lazy trailing glide. */
.gx-22__btn {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 9px;
width: 100%;
margin-top: 26px;
padding: 17px 28px;
border: 0;
border-radius: 16px;
font: inherit;
font-size: 15px;
font-weight: 600;
letter-spacing: .02em;
color: var(--ink);
cursor: pointer;
isolation: isolate;
background: radial-gradient(150% 120% at var(--gx-22-x) var(--gx-22-y),rgba(124,58,237,.28),transparent 55%), #10142b;
transition: --gx-22-x .35s ease-out,--gx-22-y .35s ease-out,transform .3s ease;
}
.gx-22__btn:active {
transform: scale(.98);
}
.gx-22__btn:focus-visible {
outline: 2px solid var(--a1);
outline-offset: 4px;
}
/* Gradient border spotlight, hollowed with a two-layer mask,
lit around the same tracked point */
.gx-22__btn::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1.5px;
background: radial-gradient(130px 90px at var(--gx-22-x) var(--gx-22-y),
var(--a1),var(--a2) 45%,rgba(148,163,184,.16) 75%);
-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;
}
/* Soft aurora bloom inside, fades up when the pointer is over */
.gx-22__btn::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: radial-gradient(140px 100px at var(--gx-22-x) var(--gx-22-y),
rgba(103,232,249,.30),rgba(167,139,250,.22) 45%,rgba(240,171,252,.10) 65%,transparent 78%);
opacity: .35;
transition: opacity .45s ease;
z-index: -1;
}
.gx-22__btn.is-lit::after {
opacity: 1;
}
.gx-22__spark {
font-size: 17px;
line-height: 1;
transform: translateY(-1px);
}
.gx-22__fine {
margin-top: 14px;
font-size: 11.5px;
letter-spacing: .03em;
color: var(--mut);
}
@media (prefers-reduced-motion: reduce) {
.gx-22__btn {
transition: none;
}
.gx-22__btn::after {
transition: none;
}
}.gx-22,
.gx-22 *,
.gx-22 *::before,
.gx-22 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.gx-22 ::selection {
background: #67e8f9;
color: #0a0d1c;
}
@property --gx-22-x {
syntax: '<percentage>';
inherits: true;
initial-value: 50%;
}
@property --gx-22-y {
syntax: '<percentage>';
inherits: true;
initial-value: 50%;
}
.gx-22 {
--bg: #0a0d1c;
--ink: #e6e9f5;
--mut: #8a91ad;
--a1: #67e8f9;
--a2: #a78bfa;
--a3: #f0abfc;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
background: radial-gradient(90% 70% at 15% 0%,rgba(103,232,249,.06),transparent 55%), radial-gradient(80% 60% at 90% 100%,rgba(167,139,250,.08),transparent 55%), var(--bg);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 48px 24px;
}
.gx-22__card {
width: min(400px,100%);
text-align: center;
padding: 36px 32px;
border-radius: 24px;
background: rgba(255,255,255,.025);
border: 1px solid rgba(148,163,184,.12);
box-shadow: 0 40px 80px -40px rgba(0,0,0,.8);
}
.gx-22__badge {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 5px 12px;
border-radius: 999px;
border: 1px solid rgba(103,232,249,.25);
font-size: 11px;
font-weight: 600;
letter-spacing: .1em;
text-transform: uppercase;
color: var(--a1);
}
.gx-22__badge::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--a1);
box-shadow: 0 0 8px var(--a1);
}
.gx-22__title {
margin-top: 18px;
font-size: 24px;
font-weight: 700;
letter-spacing: -.02em;
color: var(--ink);
}
.gx-22__copy {
margin: 10px auto 0;
max-width: 32ch;
font-size: 14px;
line-height: 1.6;
color: var(--mut);
}
/* The button. --gx-22-x / --gx-22-y are registered <percentage> custom
properties, so they INTERPOLATE: JS writes raw pointer coords, CSS
transitions them, giving the aurora its lazy trailing glide. */
.gx-22__btn {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 9px;
width: 100%;
margin-top: 26px;
padding: 17px 28px;
border: 0;
border-radius: 16px;
font: inherit;
font-size: 15px;
font-weight: 600;
letter-spacing: .02em;
color: var(--ink);
cursor: pointer;
isolation: isolate;
background: radial-gradient(150% 120% at var(--gx-22-x) var(--gx-22-y),rgba(124,58,237,.28),transparent 55%), #10142b;
transition: --gx-22-x .35s ease-out,--gx-22-y .35s ease-out,transform .3s ease;
}
.gx-22__btn:active {
transform: scale(.98);
}
.gx-22__btn:focus-visible {
outline: 2px solid var(--a1);
outline-offset: 4px;
}
/* Gradient border spotlight, hollowed with a two-layer mask,
lit around the same tracked point */
.gx-22__btn::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1.5px;
background: radial-gradient(130px 90px at var(--gx-22-x) var(--gx-22-y),
var(--a1),var(--a2) 45%,rgba(148,163,184,.16) 75%);
-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;
}
/* Soft aurora bloom inside, fades up when the pointer is over */
.gx-22__btn::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: radial-gradient(140px 100px at var(--gx-22-x) var(--gx-22-y),
rgba(103,232,249,.30),rgba(167,139,250,.22) 45%,rgba(240,171,252,.10) 65%,transparent 78%);
opacity: .35;
transition: opacity .45s ease;
z-index: -1;
}
.gx-22__btn.is-lit::after {
opacity: 1;
}
.gx-22__spark {
font-size: 17px;
line-height: 1;
transform: translateY(-1px);
}
.gx-22__fine {
margin-top: 14px;
font-size: 11.5px;
letter-spacing: .03em;
color: var(--mut);
}
@media (prefers-reduced-motion: reduce) {
.gx-22__btn {
transition: none;
}
.gx-22__btn::after {
transition: none;
}
}(function(){
var btn=document.getElementById('gx-22-btn');
if(!btn)return;
btn.addEventListener('pointermove',function(e){
var r=btn.getBoundingClientRect();
var x=Math.max(0,Math.min(100,(e.clientX-r.left)/r.width*100));
var y=Math.max(0,Math.min(100,(e.clientY-r.top)/r.height*100));
btn.style.setProperty('--gx-22-x',x.toFixed(1)+'%');
btn.style.setProperty('--gx-22-y',y.toFixed(1)+'%');
});
btn.addEventListener('pointerenter',function(){
btn.classList.add('is-lit');
});
btn.addEventListener('pointerleave',function(){
btn.classList.remove('is-lit');
/* removing the inline values lets the registered properties
transition back to their 50%/50% initial values */
btn.style.removeProperty('--gx-22-x');
btn.style.removeProperty('--gx-22-y');
});
})();(function(){
var btn=document.getElementById('gx-22-btn');
if(!btn)return;
btn.addEventListener('pointermove',function(e){
var r=btn.getBoundingClientRect();
var x=Math.max(0,Math.min(100,(e.clientX-r.left)/r.width*100));
var y=Math.max(0,Math.min(100,(e.clientY-r.top)/r.height*100));
btn.style.setProperty('--gx-22-x',x.toFixed(1)+'%');
btn.style.setProperty('--gx-22-y',y.toFixed(1)+'%');
});
btn.addEventListener('pointerenter',function(){
btn.classList.add('is-lit');
});
btn.addEventListener('pointerleave',function(){
btn.classList.remove('is-lit');
/* removing the inline values lets the registered properties
transition back to their 50%/50% initial values */
btn.style.removeProperty('--gx-22-x');
btn.style.removeProperty('--gx-22-y');
});
})();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: Cursor Tracking Aurora Gradient Button
Source: https://codefronts.com/components/css-gradient-buttons/cursor-tracking-aurora-gradient-button/
A dark glass button whose aurora core and gradient border spotlight both chase the pointer with a lazy trailing glide, then drift back to center when the cursor leaves — powered by transitions on registered @property percentages.
## HTML
```html
<div class="gx-22">
<div class="gx-22__card">
<span class="gx-22__badge">Private beta</span>
<h3 class="gx-22__title">Lumen is almost ready</h3>
<p class="gx-22__copy">A calmer way to read the internet. We let a few hundred people in every Friday.</p>
<button class="gx-22__btn" type="button" id="gx-22-btn">
<span class="gx-22__spark" aria-hidden="true">✦</span>
Request an invite
</button>
<p class="gx-22__fine">No card required · 2,418 people ahead of you</p>
</div>
</div>
```
## CSS
```css
.gx-22,
.gx-22 *,
.gx-22 *::before,
.gx-22 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.gx-22 ::selection {
background: #67e8f9;
color: #0a0d1c;
}
@property --gx-22-x {
syntax: '<percentage>';
inherits: true;
initial-value: 50%;
}
@property --gx-22-y {
syntax: '<percentage>';
inherits: true;
initial-value: 50%;
}
.gx-22 {
--bg: #0a0d1c;
--ink: #e6e9f5;
--mut: #8a91ad;
--a1: #67e8f9;
--a2: #a78bfa;
--a3: #f0abfc;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
background: radial-gradient(90% 70% at 15% 0%,rgba(103,232,249,.06),transparent 55%), radial-gradient(80% 60% at 90% 100%,rgba(167,139,250,.08),transparent 55%), var(--bg);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 48px 24px;
}
.gx-22__card {
width: min(400px,100%);
text-align: center;
padding: 36px 32px;
border-radius: 24px;
background: rgba(255,255,255,.025);
border: 1px solid rgba(148,163,184,.12);
box-shadow: 0 40px 80px -40px rgba(0,0,0,.8);
}
.gx-22__badge {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 5px 12px;
border-radius: 999px;
border: 1px solid rgba(103,232,249,.25);
font-size: 11px;
font-weight: 600;
letter-spacing: .1em;
text-transform: uppercase;
color: var(--a1);
}
.gx-22__badge::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--a1);
box-shadow: 0 0 8px var(--a1);
}
.gx-22__title {
margin-top: 18px;
font-size: 24px;
font-weight: 700;
letter-spacing: -.02em;
color: var(--ink);
}
.gx-22__copy {
margin: 10px auto 0;
max-width: 32ch;
font-size: 14px;
line-height: 1.6;
color: var(--mut);
}
/* The button. --gx-22-x / --gx-22-y are registered <percentage> custom
properties, so they INTERPOLATE: JS writes raw pointer coords, CSS
transitions them, giving the aurora its lazy trailing glide. */
.gx-22__btn {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 9px;
width: 100%;
margin-top: 26px;
padding: 17px 28px;
border: 0;
border-radius: 16px;
font: inherit;
font-size: 15px;
font-weight: 600;
letter-spacing: .02em;
color: var(--ink);
cursor: pointer;
isolation: isolate;
background: radial-gradient(150% 120% at var(--gx-22-x) var(--gx-22-y),rgba(124,58,237,.28),transparent 55%), #10142b;
transition: --gx-22-x .35s ease-out,--gx-22-y .35s ease-out,transform .3s ease;
}
.gx-22__btn:active {
transform: scale(.98);
}
.gx-22__btn:focus-visible {
outline: 2px solid var(--a1);
outline-offset: 4px;
}
/* Gradient border spotlight, hollowed with a two-layer mask,
lit around the same tracked point */
.gx-22__btn::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1.5px;
background: radial-gradient(130px 90px at var(--gx-22-x) var(--gx-22-y),
var(--a1),var(--a2) 45%,rgba(148,163,184,.16) 75%);
-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;
}
/* Soft aurora bloom inside, fades up when the pointer is over */
.gx-22__btn::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: radial-gradient(140px 100px at var(--gx-22-x) var(--gx-22-y),
rgba(103,232,249,.30),rgba(167,139,250,.22) 45%,rgba(240,171,252,.10) 65%,transparent 78%);
opacity: .35;
transition: opacity .45s ease;
z-index: -1;
}
.gx-22__btn.is-lit::after {
opacity: 1;
}
.gx-22__spark {
font-size: 17px;
line-height: 1;
transform: translateY(-1px);
}
.gx-22__fine {
margin-top: 14px;
font-size: 11.5px;
letter-spacing: .03em;
color: var(--mut);
}
@media (prefers-reduced-motion: reduce) {
.gx-22__btn {
transition: none;
}
.gx-22__btn::after {
transition: none;
}
}
```
## JavaScript
```js
(function(){
var btn=document.getElementById('gx-22-btn');
if(!btn)return;
btn.addEventListener('pointermove',function(e){
var r=btn.getBoundingClientRect();
var x=Math.max(0,Math.min(100,(e.clientX-r.left)/r.width*100));
var y=Math.max(0,Math.min(100,(e.clientY-r.top)/r.height*100));
btn.style.setProperty('--gx-22-x',x.toFixed(1)+'%');
btn.style.setProperty('--gx-22-y',y.toFixed(1)+'%');
});
btn.addEventListener('pointerenter',function(){
btn.classList.add('is-lit');
});
btn.addEventListener('pointerleave',function(){
btn.classList.remove('is-lit');
/* removing the inline values lets the registered properties
transition back to their 50%/50% initial values */
btn.style.removeProperty('--gx-22-x');
btn.style.removeProperty('--gx-22-y');
});
})();
```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: Cursor Tracking Aurora Gradient Button
Source: https://codefronts.com/components/css-gradient-buttons/cursor-tracking-aurora-gradient-button/
A dark glass button whose aurora core and gradient border spotlight both chase the pointer with a lazy trailing glide, then drift back to center when the cursor leaves — powered by transitions on registered @property percentages.
## HTML
```html
<div class="gx-22">
<div class="gx-22__card">
<span class="gx-22__badge">Private beta</span>
<h3 class="gx-22__title">Lumen is almost ready</h3>
<p class="gx-22__copy">A calmer way to read the internet. We let a few hundred people in every Friday.</p>
<button class="gx-22__btn" type="button" id="gx-22-btn">
<span class="gx-22__spark" aria-hidden="true">✦</span>
Request an invite
</button>
<p class="gx-22__fine">No card required · 2,418 people ahead of you</p>
</div>
</div>
```
## CSS
```css
.gx-22,
.gx-22 *,
.gx-22 *::before,
.gx-22 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.gx-22 ::selection {
background: #67e8f9;
color: #0a0d1c;
}
@property --gx-22-x {
syntax: '<percentage>';
inherits: true;
initial-value: 50%;
}
@property --gx-22-y {
syntax: '<percentage>';
inherits: true;
initial-value: 50%;
}
.gx-22 {
--bg: #0a0d1c;
--ink: #e6e9f5;
--mut: #8a91ad;
--a1: #67e8f9;
--a2: #a78bfa;
--a3: #f0abfc;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
background: radial-gradient(90% 70% at 15% 0%,rgba(103,232,249,.06),transparent 55%), radial-gradient(80% 60% at 90% 100%,rgba(167,139,250,.08),transparent 55%), var(--bg);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 48px 24px;
}
.gx-22__card {
width: min(400px,100%);
text-align: center;
padding: 36px 32px;
border-radius: 24px;
background: rgba(255,255,255,.025);
border: 1px solid rgba(148,163,184,.12);
box-shadow: 0 40px 80px -40px rgba(0,0,0,.8);
}
.gx-22__badge {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 5px 12px;
border-radius: 999px;
border: 1px solid rgba(103,232,249,.25);
font-size: 11px;
font-weight: 600;
letter-spacing: .1em;
text-transform: uppercase;
color: var(--a1);
}
.gx-22__badge::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--a1);
box-shadow: 0 0 8px var(--a1);
}
.gx-22__title {
margin-top: 18px;
font-size: 24px;
font-weight: 700;
letter-spacing: -.02em;
color: var(--ink);
}
.gx-22__copy {
margin: 10px auto 0;
max-width: 32ch;
font-size: 14px;
line-height: 1.6;
color: var(--mut);
}
/* The button. --gx-22-x / --gx-22-y are registered <percentage> custom
properties, so they INTERPOLATE: JS writes raw pointer coords, CSS
transitions them, giving the aurora its lazy trailing glide. */
.gx-22__btn {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 9px;
width: 100%;
margin-top: 26px;
padding: 17px 28px;
border: 0;
border-radius: 16px;
font: inherit;
font-size: 15px;
font-weight: 600;
letter-spacing: .02em;
color: var(--ink);
cursor: pointer;
isolation: isolate;
background: radial-gradient(150% 120% at var(--gx-22-x) var(--gx-22-y),rgba(124,58,237,.28),transparent 55%), #10142b;
transition: --gx-22-x .35s ease-out,--gx-22-y .35s ease-out,transform .3s ease;
}
.gx-22__btn:active {
transform: scale(.98);
}
.gx-22__btn:focus-visible {
outline: 2px solid var(--a1);
outline-offset: 4px;
}
/* Gradient border spotlight, hollowed with a two-layer mask,
lit around the same tracked point */
.gx-22__btn::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1.5px;
background: radial-gradient(130px 90px at var(--gx-22-x) var(--gx-22-y),
var(--a1),var(--a2) 45%,rgba(148,163,184,.16) 75%);
-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;
}
/* Soft aurora bloom inside, fades up when the pointer is over */
.gx-22__btn::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: radial-gradient(140px 100px at var(--gx-22-x) var(--gx-22-y),
rgba(103,232,249,.30),rgba(167,139,250,.22) 45%,rgba(240,171,252,.10) 65%,transparent 78%);
opacity: .35;
transition: opacity .45s ease;
z-index: -1;
}
.gx-22__btn.is-lit::after {
opacity: 1;
}
.gx-22__spark {
font-size: 17px;
line-height: 1;
transform: translateY(-1px);
}
.gx-22__fine {
margin-top: 14px;
font-size: 11.5px;
letter-spacing: .03em;
color: var(--mut);
}
@media (prefers-reduced-motion: reduce) {
.gx-22__btn {
transition: none;
}
.gx-22__btn::after {
transition: none;
}
}
```
## JavaScript
```js
(function(){
var btn=document.getElementById('gx-22-btn');
if(!btn)return;
btn.addEventListener('pointermove',function(e){
var r=btn.getBoundingClientRect();
var x=Math.max(0,Math.min(100,(e.clientX-r.left)/r.width*100));
var y=Math.max(0,Math.min(100,(e.clientY-r.top)/r.height*100));
btn.style.setProperty('--gx-22-x',x.toFixed(1)+'%');
btn.style.setProperty('--gx-22-y',y.toFixed(1)+'%');
});
btn.addEventListener('pointerenter',function(){
btn.classList.add('is-lit');
});
btn.addEventListener('pointerleave',function(){
btn.classList.remove('is-lit');
/* removing the inline values lets the registered properties
transition back to their 50%/50% initial values */
btn.style.removeProperty('--gx-22-x');
btn.style.removeProperty('--gx-22-y');
});
})();
```How this works
Two custom properties are registered with @property --gx-22-x { syntax: ' (and the same for y), which makes them interpolable. The JS is deliberately dumb: on pointermove it converts the cursor position to percentages and writes them inline. The CSS does the character work — transition: --gx-22-x .35s ease-out, --gx-22-y .35s ease-out means the painted point always lags the real cursor, producing the liquid trailing feel.
Three layers read the same coordinates: the button background's radial-gradient(150% 120% at var(--gx-22-x) var(--gx-22-y), ...) violet wash, a ::after aurora bloom whose opacity lifts via .is-lit, and a ::before border spotlight — a radial gradient hollowed into a 1.5px ring with the mask-composite: exclude trick. On pointerleave the JS simply removes the inline values, and the registered initial values pull everything gliding back to dead center.
Make it yours
- The trail personality is the
.35s ease-outtransition — shorten to.12sfor eager 1:1 tracking, stretch to.8sfor a dreamy, heavy glow. - Resize the spotlight independently per layer: the border ring uses
130px 90px, the bloom140px 100px, the background wash150% 120%. - Retint via
--a1/--a2/--a3; the badge dot and focus ring reuse--a1so the accent stays coherent. - For touch-first audiences, add a
pointerdownhandler that sets the coords to the tap point — taps then bloom from the finger instead of the center. - Without JS the button still renders a handsome centered aurora, because every gradient falls back to the registered
50%initial values — that's the graceful-degradation story built in.
Gotchas — read before shipping
- Transitioning registered custom properties re-resolves and repaints the gradients every frame — fine for a button-sized element, but don't lift this pattern onto a hero-sized panel without testing.
- Where
@propertyis unsupported the button never breaks: the variables still update instantly on pointermove, you just lose the interpolated glide (Firefox gained @property in 128). - Reset the coords by removing the inline properties, not by writing
50%— removal lets future CSS (media queries, themes) own the resting value again. - Clamp the computed percentages;
pointermovecan report coordinates a hair outside the rect during fast exits, which makes the ring flicker at the edges.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 85+ | 16.4+ | 128+ | 85+ |
Versions gate only the animated glide via @property; everywhere else the spotlight still tracks the cursor, it just snaps instead of gliding.