32 CSS Floating Action Button Designs 15 / 32
Magnetic Hover FAB
Magnetic hover floating action button using JS mouse tracking and lerp easing — the button pulls toward the cursor within a radius.
The code
<div class="fb15">
<h1>Magnetic Hover FAB</h1>
<p class="fb15-subtitle">Hover near each button — it pulls toward your cursor</p>
<div class="fb15-demo-grid">
<!-- Purple circle -->
<div class="fb15-demo-cell" data-mag>
<div class="fb15-mag-orbit"></div>
<button class="fb15-mag-fab fb15-mag-fab-purple" data-mag-btn aria-label="Action">
<svg viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</button>
<div class="fb15-demo-cell-label">Circle — Purple</div>
<div class="fb15-cursor-hint">hover me</div>
</div>
<!-- Blue circle -->
<div class="fb15-demo-cell" data-mag>
<div class="fb15-mag-orbit"></div>
<button class="fb15-mag-fab fb15-mag-fab-blue" data-mag-btn aria-label="Add">
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</button>
<div class="fb15-demo-cell-label">Circle — Blue</div>
<div class="fb15-cursor-hint">hover me</div>
</div>
<!-- Emerald -->
<div class="fb15-demo-cell" data-mag>
<div class="fb15-mag-orbit"></div>
<button class="fb15-mag-fab fb15-mag-fab-emerald" data-mag-btn aria-label="Check">
<svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>
</button>
<div class="fb15-demo-cell-label">Circle — Emerald</div>
<div class="fb15-cursor-hint">hover me</div>
</div>
<!-- Rose -->
<div class="fb15-demo-cell" data-mag>
<div class="fb15-mag-orbit"></div>
<button class="fb15-mag-fab fb15-mag-fab-rose" data-mag-btn aria-label="Heart">
<svg viewBox="0 0 24 24"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg>
</button>
<div class="fb15-demo-cell-label">Circle — Rose</div>
<div class="fb15-cursor-hint">hover me</div>
</div>
<!-- Rounded square -->
<div class="fb15-demo-cell" data-mag>
<button class="fb15-mag-fab fb15-mag-fab-square" data-mag-btn aria-label="Star">
<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</button>
<div class="fb15-demo-cell-label">Rounded Square</div>
<div class="fb15-cursor-hint">hover me</div>
</div>
<!-- Pill -->
<div class="fb15-demo-cell" data-mag>
<button class="fb15-mag-fab fb15-mag-fab-pill" data-mag-btn aria-label="Get started">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
Get Started
</button>
<div class="fb15-demo-cell-label">Pill Button</div>
<div class="fb15-cursor-hint">hover me</div>
</div>
</div>
<!-- Attraction zone explainer -->
<div style="max-width:860px;width:100%;display:flex;flex-direction:column;gap:8px;">
<div style="font-size:.72rem;font-weight:700;color:#475569;text-transform:uppercase;letter-spacing:.06em;">How It Works</div>
</div>
<div class="fb15-explainer">
<div class="fb15-explainer-col">
<h3>Magnetic Formula</h3>
<p>
On <code>mousemove</code>, compute the cursor offset from the FAB center.
Multiply by a <code>strength</code> factor (0–0.5) and ease toward that target using linear interpolation (lerp) each animation frame.
Outside the attraction radius the offset snaps back to 0,0 via <code>spring</code> decay.
</p>
</div>
<div class="fb15-explainer-col">
<h3>Why it feels good</h3>
<p>
The key is <em>not</em> snapping instantly. A lerp factor of ~<code>0.12</code> per frame creates a subtle lag that reads as physical weight.
Counter-rotating the inner icon by <code>−30%</code> of the translation adds depth without extra DOM elements.
</p>
</div>
</div>
</div> <div class="fb15">
<h1>Magnetic Hover FAB</h1>
<p class="fb15-subtitle">Hover near each button — it pulls toward your cursor</p>
<div class="fb15-demo-grid">
<!-- Purple circle -->
<div class="fb15-demo-cell" data-mag>
<div class="fb15-mag-orbit"></div>
<button class="fb15-mag-fab fb15-mag-fab-purple" data-mag-btn aria-label="Action">
<svg viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</button>
<div class="fb15-demo-cell-label">Circle — Purple</div>
<div class="fb15-cursor-hint">hover me</div>
</div>
<!-- Blue circle -->
<div class="fb15-demo-cell" data-mag>
<div class="fb15-mag-orbit"></div>
<button class="fb15-mag-fab fb15-mag-fab-blue" data-mag-btn aria-label="Add">
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</button>
<div class="fb15-demo-cell-label">Circle — Blue</div>
<div class="fb15-cursor-hint">hover me</div>
</div>
<!-- Emerald -->
<div class="fb15-demo-cell" data-mag>
<div class="fb15-mag-orbit"></div>
<button class="fb15-mag-fab fb15-mag-fab-emerald" data-mag-btn aria-label="Check">
<svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>
</button>
<div class="fb15-demo-cell-label">Circle — Emerald</div>
<div class="fb15-cursor-hint">hover me</div>
</div>
<!-- Rose -->
<div class="fb15-demo-cell" data-mag>
<div class="fb15-mag-orbit"></div>
<button class="fb15-mag-fab fb15-mag-fab-rose" data-mag-btn aria-label="Heart">
<svg viewBox="0 0 24 24"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg>
</button>
<div class="fb15-demo-cell-label">Circle — Rose</div>
<div class="fb15-cursor-hint">hover me</div>
</div>
<!-- Rounded square -->
<div class="fb15-demo-cell" data-mag>
<button class="fb15-mag-fab fb15-mag-fab-square" data-mag-btn aria-label="Star">
<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</button>
<div class="fb15-demo-cell-label">Rounded Square</div>
<div class="fb15-cursor-hint">hover me</div>
</div>
<!-- Pill -->
<div class="fb15-demo-cell" data-mag>
<button class="fb15-mag-fab fb15-mag-fab-pill" data-mag-btn aria-label="Get started">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
Get Started
</button>
<div class="fb15-demo-cell-label">Pill Button</div>
<div class="fb15-cursor-hint">hover me</div>
</div>
</div>
<!-- Attraction zone explainer -->
<div style="max-width:860px;width:100%;display:flex;flex-direction:column;gap:8px;">
<div style="font-size:.72rem;font-weight:700;color:#475569;text-transform:uppercase;letter-spacing:.06em;">How It Works</div>
</div>
<div class="fb15-explainer">
<div class="fb15-explainer-col">
<h3>Magnetic Formula</h3>
<p>
On <code>mousemove</code>, compute the cursor offset from the FAB center.
Multiply by a <code>strength</code> factor (0–0.5) and ease toward that target using linear interpolation (lerp) each animation frame.
Outside the attraction radius the offset snaps back to 0,0 via <code>spring</code> decay.
</p>
</div>
<div class="fb15-explainer-col">
<h3>Why it feels good</h3>
<p>
The key is <em>not</em> snapping instantly. A lerp factor of ~<code>0.12</code> per frame creates a subtle lag that reads as physical weight.
Counter-rotating the inner icon by <code>−30%</code> of the translation adds depth without extra DOM elements.
</p>
</div>
</div>
</div>.fb15, .fb15 *, .fb15 *::before, .fb15 *::after { box-sizing: border-box; margin: 0; padding: 0; }
.fb15 {
font-family: 'Syne', sans-serif;
background: #0d0d12;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding: 48px 24px;
gap: 48px;
color: #e2e8f0;
}
h1 {
font-size: 1.5rem;
font-weight: 800;
text-align: center;
background: linear-gradient(135deg, #a78bfa, #60a5fa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
p.fb15-subtitle {
color: #64748b;
font-size: 0.85rem;
text-align: center;
margin-top: -36px;
}
/* ── Demo Grid ── */
.fb15-demo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 24px;
width: 100%;
max-width: 860px;
}
.fb15-demo-cell {
background: #14141e;
border: 1px solid #1e1e2e;
border-radius: 20px;
padding: 40px 20px 28px;
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
position: relative;
overflow: visible;
}
.fb15-demo-cell-label {
font-size: 0.72rem;
font-weight: 600;
color: #475569;
letter-spacing: .05em;
text-transform: uppercase;
}
/* ── Magnetic FAB base ── */
.fb15-mag-fab {
position: relative;
width: 64px;
height: 64px;
border-radius: 50%;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
outline: none;
/* JS sets transform; CSS provides spring fallback */
transition: transform .08s linear, box-shadow .2s;
will-change: transform;
}
/* Variant: Purple */
.fb15-mag-fab-purple {
background: linear-gradient(135deg, #7c3aed, #a855f7);
box-shadow: 0 6px 24px rgba(124,58,237,.5);
}
.fb15-mag-fab-purple:hover { box-shadow: 0 10px 36px rgba(168,85,247,.6); }
/* Variant: Blue */
.fb15-mag-fab-blue {
background: linear-gradient(135deg, #2563eb, #38bdf8);
box-shadow: 0 6px 24px rgba(37,99,235,.5);
}
.fb15-mag-fab-blue:hover { box-shadow: 0 10px 36px rgba(56,189,248,.6); }
/* Variant: Emerald */
.fb15-mag-fab-emerald {
background: linear-gradient(135deg, #059669, #34d399);
box-shadow: 0 6px 24px rgba(5,150,105,.5);
}
.fb15-mag-fab-emerald:hover { box-shadow: 0 10px 36px rgba(52,211,153,.6); }
/* Variant: Rose */
.fb15-mag-fab-rose {
background: linear-gradient(135deg, #e11d48, #fb7185);
box-shadow: 0 6px 24px rgba(225,29,72,.5);
}
.fb15-mag-fab-rose:hover { box-shadow: 0 10px 36px rgba(251,113,133,.6); }
/* Variant: Square */
.fb15-mag-fab-square {
border-radius: 20px;
background: linear-gradient(135deg, #d97706, #fbbf24);
box-shadow: 0 6px 24px rgba(217,119,6,.5);
}
.fb15-mag-fab-square:hover { box-shadow: 0 10px 36px rgba(251,191,36,.6); }
/* Variant: Pill */
.fb15-mag-fab-pill {
width: 140px;
border-radius: 999px;
background: linear-gradient(135deg, #0f172a, #334155);
border: 1px solid #475569;
box-shadow: 0 4px 20px rgba(0,0,0,.4);
gap: 8px;
font-size: 0.8rem;
font-weight: 700;
color: #e2e8f0;
letter-spacing: .04em;
}
.fb15-mag-fab svg {
width: 24px;
height: 24px;
fill: none;
stroke: #fff;
stroke-width: 2.2;
stroke-linecap: round;
stroke-linejoin: round;
pointer-events: none;
/* The icon counter-rotates slightly for realism */
transition: transform .08s linear;
}
.fb15-mag-fab-pill svg { width: 20px; height: 20px; }
/* Orbit ring that follows the magnet */
.fb15-mag-orbit {
position: absolute;
inset: -10px;
border-radius: 50%;
border: 1.5px dashed rgba(255,255,255,.15);
pointer-events: none;
transition: opacity .2s;
opacity: 0;
}
.fb15-demo-cell:hover .fb15-mag-orbit { opacity: 1; }
/* Cursor dot that sticks to FAB area */
.fb15-cursor-hint {
position: absolute;
bottom: 10px;
right: 14px;
font-size: 0.65rem;
color: #334155;
font-weight: 600;
pointer-events: none;
}
/* ── Attraction zone visualization ── */
.fb15-zone-wrap {
position: relative;
width: 220px;
height: 220px;
display: flex;
align-items: center;
justify-content: center;
}
.fb15-zone-ring {
position: absolute;
border-radius: 50%;
border: 1px dashed #1e293b;
}
.fb15-zone-ring-1 { width: 80px; height: 80px; border-color: #334155; }
.fb15-zone-ring-2 { width: 130px; height: 130px; border-color: #1e293b; }
.fb15-zone-ring-3 { width: 200px; height: 200px; border-color: #1a1a2a; }
.fb15-zone-label-1, .fb15-zone-label-2, .fb15-zone-label-3 {
position: absolute;
font-size: 0.6rem;
font-weight: 600;
color: #334155;
right: 0;
transform: translateX(50%);
white-space: nowrap;
}
.fb15-zone-label-1 { top: calc(50% - 40px); }
.fb15-zone-label-2 { top: calc(50% - 65px); }
.fb15-zone-label-3 { top: calc(50% - 100px); }
/* ── Explainer card ── */
.fb15-explainer {
background: #14141e;
border: 1px solid #1e1e2e;
border-radius: 20px;
padding: 24px 28px;
max-width: 860px;
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.fb15-explainer-col h3 {
font-size: 0.8rem;
font-weight: 700;
color: #a78bfa;
text-transform: uppercase;
letter-spacing: .06em;
margin-bottom: 10px;
}
.fb15-explainer-col p, .fb15-explainer-col code {
font-size: 0.78rem;
color: #64748b;
line-height: 1.7;
font-family: 'Syne', monospace;
}
.fb15-explainer-col code {
background: #0d0d12;
color: #7dd3fc;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.73rem;
}
@media (max-width: 600px) {
.fb15-demo-grid { grid-template-columns: 1fr 1fr; }
.fb15-explainer { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
.fb15-mag-fab { transition: none !important; }
.fb15-mag-fab svg { transition: none !important; }
} .fb15, .fb15 *, .fb15 *::before, .fb15 *::after { box-sizing: border-box; margin: 0; padding: 0; }
.fb15 {
font-family: 'Syne', sans-serif;
background: #0d0d12;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding: 48px 24px;
gap: 48px;
color: #e2e8f0;
}
h1 {
font-size: 1.5rem;
font-weight: 800;
text-align: center;
background: linear-gradient(135deg, #a78bfa, #60a5fa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
p.fb15-subtitle {
color: #64748b;
font-size: 0.85rem;
text-align: center;
margin-top: -36px;
}
/* ── Demo Grid ── */
.fb15-demo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 24px;
width: 100%;
max-width: 860px;
}
.fb15-demo-cell {
background: #14141e;
border: 1px solid #1e1e2e;
border-radius: 20px;
padding: 40px 20px 28px;
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
position: relative;
overflow: visible;
}
.fb15-demo-cell-label {
font-size: 0.72rem;
font-weight: 600;
color: #475569;
letter-spacing: .05em;
text-transform: uppercase;
}
/* ── Magnetic FAB base ── */
.fb15-mag-fab {
position: relative;
width: 64px;
height: 64px;
border-radius: 50%;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
outline: none;
/* JS sets transform; CSS provides spring fallback */
transition: transform .08s linear, box-shadow .2s;
will-change: transform;
}
/* Variant: Purple */
.fb15-mag-fab-purple {
background: linear-gradient(135deg, #7c3aed, #a855f7);
box-shadow: 0 6px 24px rgba(124,58,237,.5);
}
.fb15-mag-fab-purple:hover { box-shadow: 0 10px 36px rgba(168,85,247,.6); }
/* Variant: Blue */
.fb15-mag-fab-blue {
background: linear-gradient(135deg, #2563eb, #38bdf8);
box-shadow: 0 6px 24px rgba(37,99,235,.5);
}
.fb15-mag-fab-blue:hover { box-shadow: 0 10px 36px rgba(56,189,248,.6); }
/* Variant: Emerald */
.fb15-mag-fab-emerald {
background: linear-gradient(135deg, #059669, #34d399);
box-shadow: 0 6px 24px rgba(5,150,105,.5);
}
.fb15-mag-fab-emerald:hover { box-shadow: 0 10px 36px rgba(52,211,153,.6); }
/* Variant: Rose */
.fb15-mag-fab-rose {
background: linear-gradient(135deg, #e11d48, #fb7185);
box-shadow: 0 6px 24px rgba(225,29,72,.5);
}
.fb15-mag-fab-rose:hover { box-shadow: 0 10px 36px rgba(251,113,133,.6); }
/* Variant: Square */
.fb15-mag-fab-square {
border-radius: 20px;
background: linear-gradient(135deg, #d97706, #fbbf24);
box-shadow: 0 6px 24px rgba(217,119,6,.5);
}
.fb15-mag-fab-square:hover { box-shadow: 0 10px 36px rgba(251,191,36,.6); }
/* Variant: Pill */
.fb15-mag-fab-pill {
width: 140px;
border-radius: 999px;
background: linear-gradient(135deg, #0f172a, #334155);
border: 1px solid #475569;
box-shadow: 0 4px 20px rgba(0,0,0,.4);
gap: 8px;
font-size: 0.8rem;
font-weight: 700;
color: #e2e8f0;
letter-spacing: .04em;
}
.fb15-mag-fab svg {
width: 24px;
height: 24px;
fill: none;
stroke: #fff;
stroke-width: 2.2;
stroke-linecap: round;
stroke-linejoin: round;
pointer-events: none;
/* The icon counter-rotates slightly for realism */
transition: transform .08s linear;
}
.fb15-mag-fab-pill svg { width: 20px; height: 20px; }
/* Orbit ring that follows the magnet */
.fb15-mag-orbit {
position: absolute;
inset: -10px;
border-radius: 50%;
border: 1.5px dashed rgba(255,255,255,.15);
pointer-events: none;
transition: opacity .2s;
opacity: 0;
}
.fb15-demo-cell:hover .fb15-mag-orbit { opacity: 1; }
/* Cursor dot that sticks to FAB area */
.fb15-cursor-hint {
position: absolute;
bottom: 10px;
right: 14px;
font-size: 0.65rem;
color: #334155;
font-weight: 600;
pointer-events: none;
}
/* ── Attraction zone visualization ── */
.fb15-zone-wrap {
position: relative;
width: 220px;
height: 220px;
display: flex;
align-items: center;
justify-content: center;
}
.fb15-zone-ring {
position: absolute;
border-radius: 50%;
border: 1px dashed #1e293b;
}
.fb15-zone-ring-1 { width: 80px; height: 80px; border-color: #334155; }
.fb15-zone-ring-2 { width: 130px; height: 130px; border-color: #1e293b; }
.fb15-zone-ring-3 { width: 200px; height: 200px; border-color: #1a1a2a; }
.fb15-zone-label-1, .fb15-zone-label-2, .fb15-zone-label-3 {
position: absolute;
font-size: 0.6rem;
font-weight: 600;
color: #334155;
right: 0;
transform: translateX(50%);
white-space: nowrap;
}
.fb15-zone-label-1 { top: calc(50% - 40px); }
.fb15-zone-label-2 { top: calc(50% - 65px); }
.fb15-zone-label-3 { top: calc(50% - 100px); }
/* ── Explainer card ── */
.fb15-explainer {
background: #14141e;
border: 1px solid #1e1e2e;
border-radius: 20px;
padding: 24px 28px;
max-width: 860px;
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.fb15-explainer-col h3 {
font-size: 0.8rem;
font-weight: 700;
color: #a78bfa;
text-transform: uppercase;
letter-spacing: .06em;
margin-bottom: 10px;
}
.fb15-explainer-col p, .fb15-explainer-col code {
font-size: 0.78rem;
color: #64748b;
line-height: 1.7;
font-family: 'Syne', monospace;
}
.fb15-explainer-col code {
background: #0d0d12;
color: #7dd3fc;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.73rem;
}
@media (max-width: 600px) {
.fb15-demo-grid { grid-template-columns: 1fr 1fr; }
.fb15-explainer { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
.fb15-mag-fab { transition: none !important; }
.fb15-mag-fab svg { transition: none !important; }
}// Magnetic button logic
const ATTRACT_RADIUS = 90; // px from center to start pull
const STRENGTH = 0.38; // how far it moves (0–1)
const LERP = 0.12; // easing per frame
const RELEASE_LERP = 0.08; // easing back to center
const buttons = document.querySelectorAll('[data-mag-btn]');
buttons.forEach(btn => {
let tx = 0, ty = 0; // target translation
let cx = 0, cy = 0; // current (lerped) translation
let rafId = null;
let inside = false;
const cell = btn.closest('[data-mag]');
function animate() {
const lerpF = inside ? LERP : RELEASE_LERP;
cx += (tx - cx) * lerpF;
cy += (ty - cy) * lerpF;
btn.style.transform = `translate(${cx.toFixed(2)}px, ${cy.toFixed(2)}px)`;
// Counter-rotate icon
const icon = btn.querySelector('svg');
if (icon) {
icon.style.transform = `translate(${(-cx * .3).toFixed(2)}px, ${(-cy * .3).toFixed(2)}px)`;
}
const stillMoving = Math.abs(tx - cx) > 0.1 || Math.abs(ty - cy) > 0.1;
if (stillMoving || inside) {
rafId = requestAnimationFrame(animate);
} else {
cx = 0; cy = 0;
btn.style.transform = '';
if (icon) icon.style.transform = '';
rafId = null;
}
}
function onMove(e) {
const rect = btn.getBoundingClientRect();
const bx = rect.left + rect.width / 2;
const by = rect.top + rect.height / 2;
const dx = e.clientX - bx;
const dy = e.clientY - by;
const dist = Math.hypot(dx, dy);
if (dist < ATTRACT_RADIUS) {
inside = true;
// Falloff: full pull at center, zero at radius edge
const falloff = 1 - dist / ATTRACT_RADIUS;
tx = dx * STRENGTH * falloff;
ty = dy * STRENGTH * falloff;
} else {
inside = false;
tx = 0; ty = 0;
}
if (!rafId) rafId = requestAnimationFrame(animate);
}
function onLeave() {
inside = false;
tx = 0; ty = 0;
if (!rafId) rafId = requestAnimationFrame(animate);
}
// Attach to entire cell for wider detection area
if (cell) {
cell.addEventListener('mousemove', onMove);
cell.addEventListener('mouseleave', onLeave);
} else {
btn.addEventListener('mousemove', onMove);
btn.addEventListener('mouseleave', onLeave);
}
});
// Respect reduced motion
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
if (mq.matches) {
document.querySelectorAll('[data-mag-btn]').forEach(btn => {
btn.style.transition = 'none';
});
} // Magnetic button logic
const ATTRACT_RADIUS = 90; // px from center to start pull
const STRENGTH = 0.38; // how far it moves (0–1)
const LERP = 0.12; // easing per frame
const RELEASE_LERP = 0.08; // easing back to center
const buttons = document.querySelectorAll('[data-mag-btn]');
buttons.forEach(btn => {
let tx = 0, ty = 0; // target translation
let cx = 0, cy = 0; // current (lerped) translation
let rafId = null;
let inside = false;
const cell = btn.closest('[data-mag]');
function animate() {
const lerpF = inside ? LERP : RELEASE_LERP;
cx += (tx - cx) * lerpF;
cy += (ty - cy) * lerpF;
btn.style.transform = `translate(${cx.toFixed(2)}px, ${cy.toFixed(2)}px)`;
// Counter-rotate icon
const icon = btn.querySelector('svg');
if (icon) {
icon.style.transform = `translate(${(-cx * .3).toFixed(2)}px, ${(-cy * .3).toFixed(2)}px)`;
}
const stillMoving = Math.abs(tx - cx) > 0.1 || Math.abs(ty - cy) > 0.1;
if (stillMoving || inside) {
rafId = requestAnimationFrame(animate);
} else {
cx = 0; cy = 0;
btn.style.transform = '';
if (icon) icon.style.transform = '';
rafId = null;
}
}
function onMove(e) {
const rect = btn.getBoundingClientRect();
const bx = rect.left + rect.width / 2;
const by = rect.top + rect.height / 2;
const dx = e.clientX - bx;
const dy = e.clientY - by;
const dist = Math.hypot(dx, dy);
if (dist < ATTRACT_RADIUS) {
inside = true;
// Falloff: full pull at center, zero at radius edge
const falloff = 1 - dist / ATTRACT_RADIUS;
tx = dx * STRENGTH * falloff;
ty = dy * STRENGTH * falloff;
} else {
inside = false;
tx = 0; ty = 0;
}
if (!rafId) rafId = requestAnimationFrame(animate);
}
function onLeave() {
inside = false;
tx = 0; ty = 0;
if (!rafId) rafId = requestAnimationFrame(animate);
}
// Attach to entire cell for wider detection area
if (cell) {
cell.addEventListener('mousemove', onMove);
cell.addEventListener('mouseleave', onLeave);
} else {
btn.addEventListener('mousemove', onMove);
btn.addEventListener('mouseleave', onLeave);
}
});
// Respect reduced motion
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
if (mq.matches) {
document.querySelectorAll('[data-mag-btn]').forEach(btn => {
btn.style.transition = 'none';
});
}