22 CSS Gradient Buttons02 / 22
CSS Magnetic Gradient Button
A conversion-research SaaS CTA whose gradient button attracts the cursor when it approaches — subtly translates toward the pointer while the radial hotspot tracks its exact position, then springs back to center with a soft bounce when the pointer leaves.
Published
The code
<div class="gb-02">
<div class="gb-02__card">
<p class="gb-02__kicker"><span class="gb-02__kicker-dot">★</span> Conversion research</p>
<h3 class="gb-02__title">Buttons that reach for you.</h3>
<p class="gb-02__copy">Our A/B test: magnetic buttons lift click-through 41% vs static. The physics take ~30 lines of JS. Same brand, same copy — different pull.</p>
<div class="gb-02__wrap" id="gb-02-wrap">
<button class="gb-02__btn" type="button" id="gb-02-btn">
<span class="gb-02__lab">Try the magnet</span>
<svg class="gb-02__arrow" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5 12h14M13 5l7 7-7 7"/>
</svg>
</button>
</div>
<p class="gb-02__foot">Live demo · move your cursor near the button</p>
</div>
</div><div class="gb-02">
<div class="gb-02__card">
<p class="gb-02__kicker"><span class="gb-02__kicker-dot">★</span> Conversion research</p>
<h3 class="gb-02__title">Buttons that reach for you.</h3>
<p class="gb-02__copy">Our A/B test: magnetic buttons lift click-through 41% vs static. The physics take ~30 lines of JS. Same brand, same copy — different pull.</p>
<div class="gb-02__wrap" id="gb-02-wrap">
<button class="gb-02__btn" type="button" id="gb-02-btn">
<span class="gb-02__lab">Try the magnet</span>
<svg class="gb-02__arrow" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5 12h14M13 5l7 7-7 7"/>
</svg>
</button>
</div>
<p class="gb-02__foot">Live demo · move your cursor near the button</p>
</div>
</div>.gb-02,
.gb-02 *,
.gb-02 *::before,
.gb-02 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.gb-02 ::selection {
background: #a78bfa;
color: #0b0e1a;
}
.gb-02 {
--bg: #0a0e1c;
--panel: #141a2e;
--line: rgba(148,163,184,.14);
--c1: #7c6cff;
--c2: #22d3ee;
--c3: #a78bfa;
--ink: #e2e8f0;
--mut: #8b93ab;
--gb-02-x: 0;
--gb-02-y: 0;
--gb-02-hx: 50%;
--gb-02-hy: 50%;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
background: radial-gradient(90% 120% at 20% -10%, rgba(124,108,255,.16) 0%, transparent 55%), radial-gradient(80% 100% at 100% 100%, rgba(34,211,238,.10) 0%, transparent 60%), var(--bg);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 48px 24px;
color: var(--ink);
}
.gb-02__card {
width: min(460px,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: 32px 32px 28px;
box-shadow: 0 30px 60px -30px rgba(0,0,0,.7);
text-align: center;
}
.gb-02__kicker {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 11px;
font-weight: 700;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--c3);
margin-bottom: 14px;
}
.gb-02__kicker-dot {
width: 16px;
height: 16px;
border-radius: 50%;
background: linear-gradient(135deg,var(--c1),var(--c2));
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: #fff;
}
.gb-02__title {
font-size: 22px;
font-weight: 800;
letter-spacing: -.01em;
line-height: 1.15;
margin-bottom: 10px;
}
.gb-02__copy {
font-size: 14px;
line-height: 1.55;
color: var(--mut);
margin-bottom: 28px;
}
.gb-02__wrap {
padding: 44px 60px;
display: inline-flex;
justify-content: center;
border-radius: 24px;
cursor: default;
}
.gb-02__btn {
--dur: .14s;
position: relative;
display: inline-flex;
align-items: center;
gap: 10px;
padding: 14px 26px;
border: 0;
border-radius: 14px;
color: #fff;
font-family: inherit;
font-size: 15px;
font-weight: 700;
letter-spacing: .01em;
cursor: pointer;
background: radial-gradient(180px 180px at var(--gb-02-hx) var(--gb-02-hy), rgba(255,255,255,.35), transparent 55%), linear-gradient(135deg, var(--c1) 0%, var(--c3) 55%, var(--c2) 100%);
box-shadow: 0 18px 32px -14px rgba(124,108,255,.55), 0 2px 8px -2px rgba(34,211,238,.35), inset 0 1px 0 rgba(255,255,255,.24);
transform: translate3d(calc(var(--gb-02-x) * .35px), calc(var(--gb-02-y) * .35px), 0);
transition: box-shadow var(--dur) ease, transform 0s;
will-change: transform;
}
.gb-02__btn.is-releasing {
transition: transform .55s cubic-bezier(.34,1.56,.64,1), box-shadow .3s ease;
}
.gb-02__btn::before {
content: "";
position: absolute;
inset: -6px;
border-radius: 18px;
background: linear-gradient(135deg, var(--c1), var(--c2));
opacity: 0;
filter: blur(18px);
z-index: -1;
transition: opacity .3s;
}
.gb-02__btn:hover::before {
opacity: .6;
}
.gb-02__btn:focus-visible {
outline: 2px solid #fff;
outline-offset: 3px;
}
.gb-02__lab {
position: relative;
z-index: 1;
}
.gb-02__arrow {
transition: transform .3s cubic-bezier(.34,1.56,.64,1);
position: relative;
z-index: 1;
}
.gb-02__btn:hover .gb-02__arrow {
transform: translateX(4px);
}
.gb-02__foot {
font-size: 11.5px;
color: var(--mut);
letter-spacing: .02em;
margin-top: 24px;
}
@media (prefers-reduced-motion: reduce) {
.gb-02__btn {
transform: none !important;
transition: none;
}
.gb-02__btn.is-releasing {
transition: none;
}
.gb-02__arrow {
transition: none;
}
}.gb-02,
.gb-02 *,
.gb-02 *::before,
.gb-02 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.gb-02 ::selection {
background: #a78bfa;
color: #0b0e1a;
}
.gb-02 {
--bg: #0a0e1c;
--panel: #141a2e;
--line: rgba(148,163,184,.14);
--c1: #7c6cff;
--c2: #22d3ee;
--c3: #a78bfa;
--ink: #e2e8f0;
--mut: #8b93ab;
--gb-02-x: 0;
--gb-02-y: 0;
--gb-02-hx: 50%;
--gb-02-hy: 50%;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
background: radial-gradient(90% 120% at 20% -10%, rgba(124,108,255,.16) 0%, transparent 55%), radial-gradient(80% 100% at 100% 100%, rgba(34,211,238,.10) 0%, transparent 60%), var(--bg);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 48px 24px;
color: var(--ink);
}
.gb-02__card {
width: min(460px,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: 32px 32px 28px;
box-shadow: 0 30px 60px -30px rgba(0,0,0,.7);
text-align: center;
}
.gb-02__kicker {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 11px;
font-weight: 700;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--c3);
margin-bottom: 14px;
}
.gb-02__kicker-dot {
width: 16px;
height: 16px;
border-radius: 50%;
background: linear-gradient(135deg,var(--c1),var(--c2));
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: #fff;
}
.gb-02__title {
font-size: 22px;
font-weight: 800;
letter-spacing: -.01em;
line-height: 1.15;
margin-bottom: 10px;
}
.gb-02__copy {
font-size: 14px;
line-height: 1.55;
color: var(--mut);
margin-bottom: 28px;
}
.gb-02__wrap {
padding: 44px 60px;
display: inline-flex;
justify-content: center;
border-radius: 24px;
cursor: default;
}
.gb-02__btn {
--dur: .14s;
position: relative;
display: inline-flex;
align-items: center;
gap: 10px;
padding: 14px 26px;
border: 0;
border-radius: 14px;
color: #fff;
font-family: inherit;
font-size: 15px;
font-weight: 700;
letter-spacing: .01em;
cursor: pointer;
background: radial-gradient(180px 180px at var(--gb-02-hx) var(--gb-02-hy), rgba(255,255,255,.35), transparent 55%), linear-gradient(135deg, var(--c1) 0%, var(--c3) 55%, var(--c2) 100%);
box-shadow: 0 18px 32px -14px rgba(124,108,255,.55), 0 2px 8px -2px rgba(34,211,238,.35), inset 0 1px 0 rgba(255,255,255,.24);
transform: translate3d(calc(var(--gb-02-x) * .35px), calc(var(--gb-02-y) * .35px), 0);
transition: box-shadow var(--dur) ease, transform 0s;
will-change: transform;
}
.gb-02__btn.is-releasing {
transition: transform .55s cubic-bezier(.34,1.56,.64,1), box-shadow .3s ease;
}
.gb-02__btn::before {
content: "";
position: absolute;
inset: -6px;
border-radius: 18px;
background: linear-gradient(135deg, var(--c1), var(--c2));
opacity: 0;
filter: blur(18px);
z-index: -1;
transition: opacity .3s;
}
.gb-02__btn:hover::before {
opacity: .6;
}
.gb-02__btn:focus-visible {
outline: 2px solid #fff;
outline-offset: 3px;
}
.gb-02__lab {
position: relative;
z-index: 1;
}
.gb-02__arrow {
transition: transform .3s cubic-bezier(.34,1.56,.64,1);
position: relative;
z-index: 1;
}
.gb-02__btn:hover .gb-02__arrow {
transform: translateX(4px);
}
.gb-02__foot {
font-size: 11.5px;
color: var(--mut);
letter-spacing: .02em;
margin-top: 24px;
}
@media (prefers-reduced-motion: reduce) {
.gb-02__btn {
transform: none !important;
transition: none;
}
.gb-02__btn.is-releasing {
transition: none;
}
.gb-02__arrow {
transition: none;
}
}(function(){
var wrap = document.getElementById('gb-02-wrap');
var btn = document.getElementById('gb-02-btn');
if(!wrap || !btn) return;
// Skip on touch devices — magnetic pull relies on a pointing cursor.
if(!matchMedia('(pointer: fine)').matches) return;
var rect = null;
function onEnter(){
rect = btn.getBoundingClientRect();
btn.classList.remove('is-releasing');
}
function onMove(e){
if(!rect) rect = btn.getBoundingClientRect();
var cx = rect.left + rect.width / 2;
var cy = rect.top + rect.height / 2;
var dx = e.clientX - cx;
var dy = e.clientY - cy;
btn.style.setProperty('--gb-02-x', dx);
btn.style.setProperty('--gb-02-y', dy);
var hx = ((e.clientX - rect.left) / rect.width * 100);
var hy = ((e.clientY - rect.top) / rect.height * 100);
btn.style.setProperty('--gb-02-hx', hx + '%');
btn.style.setProperty('--gb-02-hy', hy + '%');
}
function onLeave(){
btn.classList.add('is-releasing');
btn.style.setProperty('--gb-02-x', 0);
btn.style.setProperty('--gb-02-y', 0);
btn.style.setProperty('--gb-02-hx', '50%');
btn.style.setProperty('--gb-02-hy', '50%');
rect = null;
}
wrap.addEventListener('pointerenter', onEnter);
wrap.addEventListener('pointermove', onMove);
wrap.addEventListener('pointerleave', onLeave);
})();(function(){
var wrap = document.getElementById('gb-02-wrap');
var btn = document.getElementById('gb-02-btn');
if(!wrap || !btn) return;
// Skip on touch devices — magnetic pull relies on a pointing cursor.
if(!matchMedia('(pointer: fine)').matches) return;
var rect = null;
function onEnter(){
rect = btn.getBoundingClientRect();
btn.classList.remove('is-releasing');
}
function onMove(e){
if(!rect) rect = btn.getBoundingClientRect();
var cx = rect.left + rect.width / 2;
var cy = rect.top + rect.height / 2;
var dx = e.clientX - cx;
var dy = e.clientY - cy;
btn.style.setProperty('--gb-02-x', dx);
btn.style.setProperty('--gb-02-y', dy);
var hx = ((e.clientX - rect.left) / rect.width * 100);
var hy = ((e.clientY - rect.top) / rect.height * 100);
btn.style.setProperty('--gb-02-hx', hx + '%');
btn.style.setProperty('--gb-02-hy', hy + '%');
}
function onLeave(){
btn.classList.add('is-releasing');
btn.style.setProperty('--gb-02-x', 0);
btn.style.setProperty('--gb-02-y', 0);
btn.style.setProperty('--gb-02-hx', '50%');
btn.style.setProperty('--gb-02-hy', '50%');
rect = null;
}
wrap.addEventListener('pointerenter', onEnter);
wrap.addEventListener('pointermove', onMove);
wrap.addEventListener('pointerleave', onLeave);
})();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: CSS Magnetic Gradient Button
Source: https://codefronts.com/components/css-gradient-buttons/css-magnetic-gradient-button/
A conversion-research SaaS CTA whose gradient button attracts the cursor when it approaches — subtly translates toward the pointer while the radial hotspot tracks its exact position, then springs back to center with a soft bounce when the pointer leaves.
## HTML
```html
<div class="gb-02">
<div class="gb-02__card">
<p class="gb-02__kicker"><span class="gb-02__kicker-dot">★</span> Conversion research</p>
<h3 class="gb-02__title">Buttons that reach for you.</h3>
<p class="gb-02__copy">Our A/B test: magnetic buttons lift click-through 41% vs static. The physics take ~30 lines of JS. Same brand, same copy — different pull.</p>
<div class="gb-02__wrap" id="gb-02-wrap">
<button class="gb-02__btn" type="button" id="gb-02-btn">
<span class="gb-02__lab">Try the magnet</span>
<svg class="gb-02__arrow" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5 12h14M13 5l7 7-7 7"/>
</svg>
</button>
</div>
<p class="gb-02__foot">Live demo · move your cursor near the button</p>
</div>
</div>
```
## CSS
```css
.gb-02,
.gb-02 *,
.gb-02 *::before,
.gb-02 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.gb-02 ::selection {
background: #a78bfa;
color: #0b0e1a;
}
.gb-02 {
--bg: #0a0e1c;
--panel: #141a2e;
--line: rgba(148,163,184,.14);
--c1: #7c6cff;
--c2: #22d3ee;
--c3: #a78bfa;
--ink: #e2e8f0;
--mut: #8b93ab;
--gb-02-x: 0;
--gb-02-y: 0;
--gb-02-hx: 50%;
--gb-02-hy: 50%;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
background: radial-gradient(90% 120% at 20% -10%, rgba(124,108,255,.16) 0%, transparent 55%), radial-gradient(80% 100% at 100% 100%, rgba(34,211,238,.10) 0%, transparent 60%), var(--bg);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 48px 24px;
color: var(--ink);
}
.gb-02__card {
width: min(460px,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: 32px 32px 28px;
box-shadow: 0 30px 60px -30px rgba(0,0,0,.7);
text-align: center;
}
.gb-02__kicker {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 11px;
font-weight: 700;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--c3);
margin-bottom: 14px;
}
.gb-02__kicker-dot {
width: 16px;
height: 16px;
border-radius: 50%;
background: linear-gradient(135deg,var(--c1),var(--c2));
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: #fff;
}
.gb-02__title {
font-size: 22px;
font-weight: 800;
letter-spacing: -.01em;
line-height: 1.15;
margin-bottom: 10px;
}
.gb-02__copy {
font-size: 14px;
line-height: 1.55;
color: var(--mut);
margin-bottom: 28px;
}
.gb-02__wrap {
padding: 44px 60px;
display: inline-flex;
justify-content: center;
border-radius: 24px;
cursor: default;
}
.gb-02__btn {
--dur: .14s;
position: relative;
display: inline-flex;
align-items: center;
gap: 10px;
padding: 14px 26px;
border: 0;
border-radius: 14px;
color: #fff;
font-family: inherit;
font-size: 15px;
font-weight: 700;
letter-spacing: .01em;
cursor: pointer;
background: radial-gradient(180px 180px at var(--gb-02-hx) var(--gb-02-hy), rgba(255,255,255,.35), transparent 55%), linear-gradient(135deg, var(--c1) 0%, var(--c3) 55%, var(--c2) 100%);
box-shadow: 0 18px 32px -14px rgba(124,108,255,.55), 0 2px 8px -2px rgba(34,211,238,.35), inset 0 1px 0 rgba(255,255,255,.24);
transform: translate3d(calc(var(--gb-02-x) * .35px), calc(var(--gb-02-y) * .35px), 0);
transition: box-shadow var(--dur) ease, transform 0s;
will-change: transform;
}
.gb-02__btn.is-releasing {
transition: transform .55s cubic-bezier(.34,1.56,.64,1), box-shadow .3s ease;
}
.gb-02__btn::before {
content: "";
position: absolute;
inset: -6px;
border-radius: 18px;
background: linear-gradient(135deg, var(--c1), var(--c2));
opacity: 0;
filter: blur(18px);
z-index: -1;
transition: opacity .3s;
}
.gb-02__btn:hover::before {
opacity: .6;
}
.gb-02__btn:focus-visible {
outline: 2px solid #fff;
outline-offset: 3px;
}
.gb-02__lab {
position: relative;
z-index: 1;
}
.gb-02__arrow {
transition: transform .3s cubic-bezier(.34,1.56,.64,1);
position: relative;
z-index: 1;
}
.gb-02__btn:hover .gb-02__arrow {
transform: translateX(4px);
}
.gb-02__foot {
font-size: 11.5px;
color: var(--mut);
letter-spacing: .02em;
margin-top: 24px;
}
@media (prefers-reduced-motion: reduce) {
.gb-02__btn {
transform: none !important;
transition: none;
}
.gb-02__btn.is-releasing {
transition: none;
}
.gb-02__arrow {
transition: none;
}
}
```
## JavaScript
```js
(function(){
var wrap = document.getElementById('gb-02-wrap');
var btn = document.getElementById('gb-02-btn');
if(!wrap || !btn) return;
// Skip on touch devices — magnetic pull relies on a pointing cursor.
if(!matchMedia('(pointer: fine)').matches) return;
var rect = null;
function onEnter(){
rect = btn.getBoundingClientRect();
btn.classList.remove('is-releasing');
}
function onMove(e){
if(!rect) rect = btn.getBoundingClientRect();
var cx = rect.left + rect.width / 2;
var cy = rect.top + rect.height / 2;
var dx = e.clientX - cx;
var dy = e.clientY - cy;
btn.style.setProperty('--gb-02-x', dx);
btn.style.setProperty('--gb-02-y', dy);
var hx = ((e.clientX - rect.left) / rect.width * 100);
var hy = ((e.clientY - rect.top) / rect.height * 100);
btn.style.setProperty('--gb-02-hx', hx + '%');
btn.style.setProperty('--gb-02-hy', hy + '%');
}
function onLeave(){
btn.classList.add('is-releasing');
btn.style.setProperty('--gb-02-x', 0);
btn.style.setProperty('--gb-02-y', 0);
btn.style.setProperty('--gb-02-hx', '50%');
btn.style.setProperty('--gb-02-hy', '50%');
rect = null;
}
wrap.addEventListener('pointerenter', onEnter);
wrap.addEventListener('pointermove', onMove);
wrap.addEventListener('pointerleave', onLeave);
})();
```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: CSS Magnetic Gradient Button
Source: https://codefronts.com/components/css-gradient-buttons/css-magnetic-gradient-button/
A conversion-research SaaS CTA whose gradient button attracts the cursor when it approaches — subtly translates toward the pointer while the radial hotspot tracks its exact position, then springs back to center with a soft bounce when the pointer leaves.
## HTML
```html
<div class="gb-02">
<div class="gb-02__card">
<p class="gb-02__kicker"><span class="gb-02__kicker-dot">★</span> Conversion research</p>
<h3 class="gb-02__title">Buttons that reach for you.</h3>
<p class="gb-02__copy">Our A/B test: magnetic buttons lift click-through 41% vs static. The physics take ~30 lines of JS. Same brand, same copy — different pull.</p>
<div class="gb-02__wrap" id="gb-02-wrap">
<button class="gb-02__btn" type="button" id="gb-02-btn">
<span class="gb-02__lab">Try the magnet</span>
<svg class="gb-02__arrow" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5 12h14M13 5l7 7-7 7"/>
</svg>
</button>
</div>
<p class="gb-02__foot">Live demo · move your cursor near the button</p>
</div>
</div>
```
## CSS
```css
.gb-02,
.gb-02 *,
.gb-02 *::before,
.gb-02 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.gb-02 ::selection {
background: #a78bfa;
color: #0b0e1a;
}
.gb-02 {
--bg: #0a0e1c;
--panel: #141a2e;
--line: rgba(148,163,184,.14);
--c1: #7c6cff;
--c2: #22d3ee;
--c3: #a78bfa;
--ink: #e2e8f0;
--mut: #8b93ab;
--gb-02-x: 0;
--gb-02-y: 0;
--gb-02-hx: 50%;
--gb-02-hy: 50%;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
background: radial-gradient(90% 120% at 20% -10%, rgba(124,108,255,.16) 0%, transparent 55%), radial-gradient(80% 100% at 100% 100%, rgba(34,211,238,.10) 0%, transparent 60%), var(--bg);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 48px 24px;
color: var(--ink);
}
.gb-02__card {
width: min(460px,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: 32px 32px 28px;
box-shadow: 0 30px 60px -30px rgba(0,0,0,.7);
text-align: center;
}
.gb-02__kicker {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 11px;
font-weight: 700;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--c3);
margin-bottom: 14px;
}
.gb-02__kicker-dot {
width: 16px;
height: 16px;
border-radius: 50%;
background: linear-gradient(135deg,var(--c1),var(--c2));
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: #fff;
}
.gb-02__title {
font-size: 22px;
font-weight: 800;
letter-spacing: -.01em;
line-height: 1.15;
margin-bottom: 10px;
}
.gb-02__copy {
font-size: 14px;
line-height: 1.55;
color: var(--mut);
margin-bottom: 28px;
}
.gb-02__wrap {
padding: 44px 60px;
display: inline-flex;
justify-content: center;
border-radius: 24px;
cursor: default;
}
.gb-02__btn {
--dur: .14s;
position: relative;
display: inline-flex;
align-items: center;
gap: 10px;
padding: 14px 26px;
border: 0;
border-radius: 14px;
color: #fff;
font-family: inherit;
font-size: 15px;
font-weight: 700;
letter-spacing: .01em;
cursor: pointer;
background: radial-gradient(180px 180px at var(--gb-02-hx) var(--gb-02-hy), rgba(255,255,255,.35), transparent 55%), linear-gradient(135deg, var(--c1) 0%, var(--c3) 55%, var(--c2) 100%);
box-shadow: 0 18px 32px -14px rgba(124,108,255,.55), 0 2px 8px -2px rgba(34,211,238,.35), inset 0 1px 0 rgba(255,255,255,.24);
transform: translate3d(calc(var(--gb-02-x) * .35px), calc(var(--gb-02-y) * .35px), 0);
transition: box-shadow var(--dur) ease, transform 0s;
will-change: transform;
}
.gb-02__btn.is-releasing {
transition: transform .55s cubic-bezier(.34,1.56,.64,1), box-shadow .3s ease;
}
.gb-02__btn::before {
content: "";
position: absolute;
inset: -6px;
border-radius: 18px;
background: linear-gradient(135deg, var(--c1), var(--c2));
opacity: 0;
filter: blur(18px);
z-index: -1;
transition: opacity .3s;
}
.gb-02__btn:hover::before {
opacity: .6;
}
.gb-02__btn:focus-visible {
outline: 2px solid #fff;
outline-offset: 3px;
}
.gb-02__lab {
position: relative;
z-index: 1;
}
.gb-02__arrow {
transition: transform .3s cubic-bezier(.34,1.56,.64,1);
position: relative;
z-index: 1;
}
.gb-02__btn:hover .gb-02__arrow {
transform: translateX(4px);
}
.gb-02__foot {
font-size: 11.5px;
color: var(--mut);
letter-spacing: .02em;
margin-top: 24px;
}
@media (prefers-reduced-motion: reduce) {
.gb-02__btn {
transform: none !important;
transition: none;
}
.gb-02__btn.is-releasing {
transition: none;
}
.gb-02__arrow {
transition: none;
}
}
```
## JavaScript
```js
(function(){
var wrap = document.getElementById('gb-02-wrap');
var btn = document.getElementById('gb-02-btn');
if(!wrap || !btn) return;
// Skip on touch devices — magnetic pull relies on a pointing cursor.
if(!matchMedia('(pointer: fine)').matches) return;
var rect = null;
function onEnter(){
rect = btn.getBoundingClientRect();
btn.classList.remove('is-releasing');
}
function onMove(e){
if(!rect) rect = btn.getBoundingClientRect();
var cx = rect.left + rect.width / 2;
var cy = rect.top + rect.height / 2;
var dx = e.clientX - cx;
var dy = e.clientY - cy;
btn.style.setProperty('--gb-02-x', dx);
btn.style.setProperty('--gb-02-y', dy);
var hx = ((e.clientX - rect.left) / rect.width * 100);
var hy = ((e.clientY - rect.top) / rect.height * 100);
btn.style.setProperty('--gb-02-hx', hx + '%');
btn.style.setProperty('--gb-02-hy', hy + '%');
}
function onLeave(){
btn.classList.add('is-releasing');
btn.style.setProperty('--gb-02-x', 0);
btn.style.setProperty('--gb-02-y', 0);
btn.style.setProperty('--gb-02-hx', '50%');
btn.style.setProperty('--gb-02-hy', '50%');
rect = null;
}
wrap.addEventListener('pointerenter', onEnter);
wrap.addEventListener('pointermove', onMove);
wrap.addEventListener('pointerleave', onLeave);
})();
```How this works
The magnetic pull is driven by a single pointermove listener on the button's outer wrapper. On each move, we read the pointer's coordinates via event.clientX/clientY, subtract the button's center from getBoundingClientRect(), and compute the delta on both axes. That delta is written into two CSS custom properties — --gb-02-x and --gb-02-y — which drive the button's translate3d(calc(var(--gb-02-x) * 0.35), calc(var(--gb-02-y) * 0.35), 0).
The gradient's hotspot follows the raw pointer position (not the button's translated position) via a radial-gradient(circle at var(--gb-02-hx) var(--gb-02-hy), ...), so it feels like the pointer is illuminating the button from wherever it is. On pointerleave, we clear the CSS vars and add a .is-releasing class that transitions the transform back to zero on a springy cubic-bezier(.34, 1.56, .64, 1) curve — the button visibly bounces back to center.
Motion budget: the JS runs at pointer-event cadence (browser-throttled, ~60Hz on modern devices), writes exactly two CSS vars per frame, and does no layout reads inside the handler after the initial rect cache. Total JS: about 30 lines.
Make it yours
- Change the magnetic pull strength by editing the
0.35multiplier in thetranslate3dexpression —0.5is a stronger pull,0.2is subtler. - Adjust the release spring by tuning the
cubic-bezier(.34, 1.56, .64, 1)curve on.is-releasing— the third and fourth values above 1 create the overshoot bounce. - Retint by editing
--c1/--c2/--c3on.gb-02. The gradient hotspot, button background, and ambient glow all inherit. - Widen the magnetic sensing area by adding
padding: 60pxto.gb-02__wrap— the wrap is what receives pointer events, so a larger padding = attraction from further away. - Add a click ripple by listening for
clickand appending a<span>at the pointer coords with a scale-and-fade keyframe — pairs naturally with the magnetic pull.
Gotchas — read before shipping
- Cache the button's bounding rect ONCE on
pointerenter— not on everypointermove. ReadinggetBoundingClientRect()per frame forces a layout on any page with reflow-triggering CSS elsewhere. - Guard the JS with
if (matchMedia('(pointer: fine)').matches)so touch devices don't try to run magnetic tracking — mobile users don't have a hoverable cursor, and the effect degrades to nothing anyway. - On
pointerleave, the transitions ontransformwould normally not apply because inline styles change instantly. That's why the release path swaps to the.is-releasingclass — the class carries atransition: transform .55s cubic-bezier(...)rule that only applies during the release, not during the pursuit.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 70+ | 13+ | 68+ | 70+ |
Pointer Events + CSS custom properties in transform/background-position are baseline everywhere backdrop-filter is.