23 / 34
All 34 in 34 CSS Floating Buttons
- 01Material Design 3 Floating Action Button (FAB)
- 02iOS-Style Compose Floating Button
- 03WhatsApp Floating Message Button CSS
- 04Intercom / Drift Chat Bubble Floating Button
- 05Accessible WAI-ARIA Floating Action Button (WCAG 2.2)
- 06Extended FAB with Icon and Label (Material Design 3)
- 07Speed Dial Radial Menu FAB with Backdrop
- 08Pure CSS FAB Speed Dial (No JavaScript)
- 09Material Gradient Floating Action Button CSS
- 10Vertical and Horizontal Speed Dial FAB Menu CSS
- 11Morphing FAB Button That Expands to Pill CSS
- 12Scroll-to-Top Floating Button with SVG Progress Ring
- 13Floating Chat Widget FAB with Notification Panel
- 14Fan and Arc Radial Spread FAB Menu CSS
- 15Apple Squircle Superellipse Floating Button CSS
- 16Social Share Floating Button Cluster CSS
- 17Glassmorphism Floating Action Button with Backdrop Blur
- 18Cyberpunk Neon Glow Floating Button CSS
- 19Floating Compose Button with Tooltip on Hover CSS
- 20Draggable Floating Action Button with Corner Snap CSS
- 21Notification Bell Floating Button with Badge Counter
- 22Floating Cart Button with Live Badge Counter CSS
- 23Magnetic Cursor Follower Floating Button CSS
- 24Dark Mode Theme Toggle Floating Button CSS
- 25Classic Gradient Circle Plus FAB CSS
- 26Pulse Ring Attention-Getter Floating Button CSS
- 27Pill-Shaped FAB with Icon and Text Label CSS
- 28Neo-Brutalist Hard Shadow Floating Button CSS
- 29Floating Button with Loading Spinner State CSS
- 30Draggable Floating Button with 6-Dot Grip Handle CSS
- 31Voice Recording Microphone Floating Button CSS
- 32Aurora Gradient Drift Premium Floating Button CSS
- 33Quick Emoji Reply Reaction Floating Button CSS
- 34Modern Squircle Rounded Square FAB CSS
Magnetic Cursor Follower Floating Button CSS
The award-site magnetic button, built properly: the button leans toward the pointer within a proximity field, the icon lags slightly behind the shell for parallax depth, and everything springs home on exit. Pointer-only by design — it detects fine pointers and hover capability and skips the whole effect on touch, where it would just be dead weight.
Nucleo: 44,000+ SVG icons in five families · All Icons Bundle $149
Nucleo · Affiliate link
The code
<section class="fb-23" aria-label="Magnetic cursor follower button demo">
<div class="fb-23__stage">
<p class="fb-23__eyebrow">proximity field · linear falloff</p>
<h2 class="fb-23__title">It leans toward you</h2>
<p class="fb-23__sub">Move the pointer near the button — not onto it. The shell follows at 34%, the icon at 16%, and the difference is what gives it depth.</p>
<div class="fb-23__field" id="fb-23-field">
<span class="fb-23__halo" aria-hidden="true"></span>
<button class="fb-23__fab" id="fb-23-fab" type="button" aria-label="Start a project">
<span class="fb-23__ico" id="fb-23-ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M5 12h14M13 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
</button>
</div>
<p class="fb-23__chip" id="fb-23-chip">pointer: fine · pull 0.34 / 0.16 · spring return 520ms</p>
</div>
</section><section class="fb-23" aria-label="Magnetic cursor follower button demo">
<div class="fb-23__stage">
<p class="fb-23__eyebrow">proximity field · linear falloff</p>
<h2 class="fb-23__title">It leans toward you</h2>
<p class="fb-23__sub">Move the pointer near the button — not onto it. The shell follows at 34%, the icon at 16%, and the difference is what gives it depth.</p>
<div class="fb-23__field" id="fb-23-field">
<span class="fb-23__halo" aria-hidden="true"></span>
<button class="fb-23__fab" id="fb-23-fab" type="button" aria-label="Start a project">
<span class="fb-23__ico" id="fb-23-ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M5 12h14M13 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
</button>
</div>
<p class="fb-23__chip" id="fb-23-chip">pointer: fine · pull 0.34 / 0.16 · spring return 520ms</p>
</div>
</section>.fb-23 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-23-bg);
--fb-23-bg: radial-gradient(90% 70% at 50% 42%,oklch(0.29 0.03 285),oklch(0.14 0.015 285) 70%);
--fb-23-accent: oklch(0.86 0.18 95);
--fb-23-ink: oklch(0.97 0.008 285);
--fb-23-mut: oklch(0.74 0.015 285);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-23-ink);
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
}
.fb-23 *,
.fb-23 *::before,
.fb-23 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-23__stage {
width: min(100%,58ch);
display: grid;
gap: clamp(16px,3vw,26px);
justify-items: center;
text-align: center;
}
.fb-23__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--fb-23-accent);
}
.fb-23__title {
font-size: clamp(27px,5vw,48px);
line-height: 1.04;
letter-spacing: -.035em;
font-weight: 700;
text-wrap: balance;
}
.fb-23__sub {
max-width: 48ch;
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-23-mut);
text-wrap: pretty;
}
.fb-23__field {
position: relative;
display: grid;
place-items: center;
width: min(88vw,340px);
height: min(38svh,240px);
border-radius: 28px;
}
.fb-23__halo {
position: absolute;
width: 230px;
height: 230px;
border-radius: 50%;
border: 1px dashed oklch(1 0 0/.14);
opacity: .7;
}
.fb-23__halo::after {
content: "";
position: absolute;
inset: 44px;
border-radius: 50%;
border: 1px dashed oklch(1 0 0/.1);
}
.fb-23__fab {
position: relative;
display: grid;
place-items: center;
inline-size: 96px;
block-size: 96px;
border: 0;
border-radius: 50%;
cursor: pointer;
color: oklch(0.17 0.04 95);
background: radial-gradient(120% 120% at 32% 26%,oklch(0.96 0.13 100),var(--fb-23-accent));
box-shadow: 0 22px 60px -18px oklch(0.86 0.18 95/.5),0 3px 10px oklch(0.08 0.01 285/.7);
transition: translate .52s cubic-bezier(.2,1.5,.35,1),scale .3s cubic-bezier(.2,1.4,.4,1),box-shadow .3s ease;
}
.fb-23__fab[data-following="true"] {
transition: scale .3s cubic-bezier(.2,1.4,.4,1),box-shadow .3s ease;
}
.fb-23__fab svg {
width: 32px;
height: 32px;
}
.fb-23__ico {
display: grid;
place-items: center;
transition: translate .52s cubic-bezier(.2,1.5,.35,1);
}
.fb-23__fab[data-following="true"] .fb-23__ico {
transition: none;
}
.fb-23__fab:hover {
scale: 1.06;
box-shadow: 0 30px 76px -18px oklch(0.86 0.18 95/.6),0 3px 10px oklch(0.08 0.01 285/.7);
}
.fb-23__fab:active {
scale: .95;
}
.fb-23__fab:focus-visible {
outline: 3px solid var(--fb-23-accent);
outline-offset: 8px;
}
.fb-23__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-23-mut);
padding: 8px 14px;
border: 1px solid oklch(1 0 0/.16);
border-radius: 99px;
}
@media (prefers-reduced-motion: reduce) {
.fb-23__fab,
.fb-23__ico {
transition: none !important;
}
}.fb-23 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-23-bg);
--fb-23-bg: radial-gradient(90% 70% at 50% 42%,oklch(0.29 0.03 285),oklch(0.14 0.015 285) 70%);
--fb-23-accent: oklch(0.86 0.18 95);
--fb-23-ink: oklch(0.97 0.008 285);
--fb-23-mut: oklch(0.74 0.015 285);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-23-ink);
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
}
.fb-23 *,
.fb-23 *::before,
.fb-23 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-23__stage {
width: min(100%,58ch);
display: grid;
gap: clamp(16px,3vw,26px);
justify-items: center;
text-align: center;
}
.fb-23__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--fb-23-accent);
}
.fb-23__title {
font-size: clamp(27px,5vw,48px);
line-height: 1.04;
letter-spacing: -.035em;
font-weight: 700;
text-wrap: balance;
}
.fb-23__sub {
max-width: 48ch;
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-23-mut);
text-wrap: pretty;
}
.fb-23__field {
position: relative;
display: grid;
place-items: center;
width: min(88vw,340px);
height: min(38svh,240px);
border-radius: 28px;
}
.fb-23__halo {
position: absolute;
width: 230px;
height: 230px;
border-radius: 50%;
border: 1px dashed oklch(1 0 0/.14);
opacity: .7;
}
.fb-23__halo::after {
content: "";
position: absolute;
inset: 44px;
border-radius: 50%;
border: 1px dashed oklch(1 0 0/.1);
}
.fb-23__fab {
position: relative;
display: grid;
place-items: center;
inline-size: 96px;
block-size: 96px;
border: 0;
border-radius: 50%;
cursor: pointer;
color: oklch(0.17 0.04 95);
background: radial-gradient(120% 120% at 32% 26%,oklch(0.96 0.13 100),var(--fb-23-accent));
box-shadow: 0 22px 60px -18px oklch(0.86 0.18 95/.5),0 3px 10px oklch(0.08 0.01 285/.7);
transition: translate .52s cubic-bezier(.2,1.5,.35,1),scale .3s cubic-bezier(.2,1.4,.4,1),box-shadow .3s ease;
}
.fb-23__fab[data-following="true"] {
transition: scale .3s cubic-bezier(.2,1.4,.4,1),box-shadow .3s ease;
}
.fb-23__fab svg {
width: 32px;
height: 32px;
}
.fb-23__ico {
display: grid;
place-items: center;
transition: translate .52s cubic-bezier(.2,1.5,.35,1);
}
.fb-23__fab[data-following="true"] .fb-23__ico {
transition: none;
}
.fb-23__fab:hover {
scale: 1.06;
box-shadow: 0 30px 76px -18px oklch(0.86 0.18 95/.6),0 3px 10px oklch(0.08 0.01 285/.7);
}
.fb-23__fab:active {
scale: .95;
}
.fb-23__fab:focus-visible {
outline: 3px solid var(--fb-23-accent);
outline-offset: 8px;
}
.fb-23__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-23-mut);
padding: 8px 14px;
border: 1px solid oklch(1 0 0/.16);
border-radius: 99px;
}
@media (prefers-reduced-motion: reduce) {
.fb-23__fab,
.fb-23__ico {
transition: none !important;
}
}(() => {
const field = document.getElementById('fb-23-field');
const fab = document.getElementById('fb-23-fab');
const ico = document.getElementById('fb-23-ico');
const chip = document.getElementById('fb-23-chip');
if (!field || !fab || fab.dataset.fbWired) return;
fab.dataset.fbWired = '1';
const fine = window.matchMedia('(hover:hover) and (pointer:fine)').matches;
const calm = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!fine || calm) {
chip.textContent = calm ? 'reduced motion — magnetism disabled' : 'coarse pointer — magnetism disabled on touch';
return;
}
const RADIUS = 165;
let box = null;
const measure = () => { box = fab.getBoundingClientRect(); };
field.addEventListener('pointerenter', measure);
window.addEventListener('resize', () => { box = null; });
field.addEventListener('pointermove', (e) => {
if (!box) measure();
const cx = box.left + box.width / 2, cy = box.top + box.height / 2;
const dx = e.clientX - cx, dy = e.clientY - cy;
const pull = Math.max(0, 1 - Math.hypot(dx, dy) / RADIUS);
if (pull <= 0) return;
fab.dataset.following = 'true';
fab.style.translate = (dx * pull * 0.34).toFixed(1) + 'px ' + (dy * pull * 0.34).toFixed(1) + 'px';
ico.style.translate = (dx * pull * 0.16).toFixed(1) + 'px ' + (dy * pull * 0.16).toFixed(1) + 'px';
});
field.addEventListener('pointerleave', () => {
fab.dataset.following = 'false';
fab.style.translate = '';
ico.style.translate = '';
box = null;
});
})();(() => {
const field = document.getElementById('fb-23-field');
const fab = document.getElementById('fb-23-fab');
const ico = document.getElementById('fb-23-ico');
const chip = document.getElementById('fb-23-chip');
if (!field || !fab || fab.dataset.fbWired) return;
fab.dataset.fbWired = '1';
const fine = window.matchMedia('(hover:hover) and (pointer:fine)').matches;
const calm = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!fine || calm) {
chip.textContent = calm ? 'reduced motion — magnetism disabled' : 'coarse pointer — magnetism disabled on touch';
return;
}
const RADIUS = 165;
let box = null;
const measure = () => { box = fab.getBoundingClientRect(); };
field.addEventListener('pointerenter', measure);
window.addEventListener('resize', () => { box = null; });
field.addEventListener('pointermove', (e) => {
if (!box) measure();
const cx = box.left + box.width / 2, cy = box.top + box.height / 2;
const dx = e.clientX - cx, dy = e.clientY - cy;
const pull = Math.max(0, 1 - Math.hypot(dx, dy) / RADIUS);
if (pull <= 0) return;
fab.dataset.following = 'true';
fab.style.translate = (dx * pull * 0.34).toFixed(1) + 'px ' + (dy * pull * 0.34).toFixed(1) + 'px';
ico.style.translate = (dx * pull * 0.16).toFixed(1) + 'px ' + (dy * pull * 0.16).toFixed(1) + 'px';
});
field.addEventListener('pointerleave', () => {
fab.dataset.following = 'false';
fab.style.translate = '';
ico.style.translate = '';
box = null;
});
})();Here's a working CSS Floating 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: Magnetic Cursor Follower Floating Button CSS
Source: https://codefronts.com/components/css-floating-buttons/magnetic-hover-fab/
The award-site magnetic button, built properly: the button leans toward the pointer within a proximity field, the icon lags slightly behind the shell for parallax depth, and everything springs home on exit. Pointer-only by design — it detects fine pointers and hover capability and skips the whole effect on touch, where it would just be dead weight.
## HTML
```html
<section class="fb-23" aria-label="Magnetic cursor follower button demo">
<div class="fb-23__stage">
<p class="fb-23__eyebrow">proximity field · linear falloff</p>
<h2 class="fb-23__title">It leans toward you</h2>
<p class="fb-23__sub">Move the pointer near the button — not onto it. The shell follows at 34%, the icon at 16%, and the difference is what gives it depth.</p>
<div class="fb-23__field" id="fb-23-field">
<span class="fb-23__halo" aria-hidden="true"></span>
<button class="fb-23__fab" id="fb-23-fab" type="button" aria-label="Start a project">
<span class="fb-23__ico" id="fb-23-ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M5 12h14M13 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
</button>
</div>
<p class="fb-23__chip" id="fb-23-chip">pointer: fine · pull 0.34 / 0.16 · spring return 520ms</p>
</div>
</section>
```
## CSS
```css
.fb-23 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-23-bg);
--fb-23-bg: radial-gradient(90% 70% at 50% 42%,oklch(0.29 0.03 285),oklch(0.14 0.015 285) 70%);
--fb-23-accent: oklch(0.86 0.18 95);
--fb-23-ink: oklch(0.97 0.008 285);
--fb-23-mut: oklch(0.74 0.015 285);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-23-ink);
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
}
.fb-23 *,
.fb-23 *::before,
.fb-23 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-23__stage {
width: min(100%,58ch);
display: grid;
gap: clamp(16px,3vw,26px);
justify-items: center;
text-align: center;
}
.fb-23__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--fb-23-accent);
}
.fb-23__title {
font-size: clamp(27px,5vw,48px);
line-height: 1.04;
letter-spacing: -.035em;
font-weight: 700;
text-wrap: balance;
}
.fb-23__sub {
max-width: 48ch;
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-23-mut);
text-wrap: pretty;
}
.fb-23__field {
position: relative;
display: grid;
place-items: center;
width: min(88vw,340px);
height: min(38svh,240px);
border-radius: 28px;
}
.fb-23__halo {
position: absolute;
width: 230px;
height: 230px;
border-radius: 50%;
border: 1px dashed oklch(1 0 0/.14);
opacity: .7;
}
.fb-23__halo::after {
content: "";
position: absolute;
inset: 44px;
border-radius: 50%;
border: 1px dashed oklch(1 0 0/.1);
}
.fb-23__fab {
position: relative;
display: grid;
place-items: center;
inline-size: 96px;
block-size: 96px;
border: 0;
border-radius: 50%;
cursor: pointer;
color: oklch(0.17 0.04 95);
background: radial-gradient(120% 120% at 32% 26%,oklch(0.96 0.13 100),var(--fb-23-accent));
box-shadow: 0 22px 60px -18px oklch(0.86 0.18 95/.5),0 3px 10px oklch(0.08 0.01 285/.7);
transition: translate .52s cubic-bezier(.2,1.5,.35,1),scale .3s cubic-bezier(.2,1.4,.4,1),box-shadow .3s ease;
}
.fb-23__fab[data-following="true"] {
transition: scale .3s cubic-bezier(.2,1.4,.4,1),box-shadow .3s ease;
}
.fb-23__fab svg {
width: 32px;
height: 32px;
}
.fb-23__ico {
display: grid;
place-items: center;
transition: translate .52s cubic-bezier(.2,1.5,.35,1);
}
.fb-23__fab[data-following="true"] .fb-23__ico {
transition: none;
}
.fb-23__fab:hover {
scale: 1.06;
box-shadow: 0 30px 76px -18px oklch(0.86 0.18 95/.6),0 3px 10px oklch(0.08 0.01 285/.7);
}
.fb-23__fab:active {
scale: .95;
}
.fb-23__fab:focus-visible {
outline: 3px solid var(--fb-23-accent);
outline-offset: 8px;
}
.fb-23__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-23-mut);
padding: 8px 14px;
border: 1px solid oklch(1 0 0/.16);
border-radius: 99px;
}
@media (prefers-reduced-motion: reduce) {
.fb-23__fab,
.fb-23__ico {
transition: none !important;
}
}
```
## JavaScript
```js
(() => {
const field = document.getElementById('fb-23-field');
const fab = document.getElementById('fb-23-fab');
const ico = document.getElementById('fb-23-ico');
const chip = document.getElementById('fb-23-chip');
if (!field || !fab || fab.dataset.fbWired) return;
fab.dataset.fbWired = '1';
const fine = window.matchMedia('(hover:hover) and (pointer:fine)').matches;
const calm = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!fine || calm) {
chip.textContent = calm ? 'reduced motion — magnetism disabled' : 'coarse pointer — magnetism disabled on touch';
return;
}
const RADIUS = 165;
let box = null;
const measure = () => { box = fab.getBoundingClientRect(); };
field.addEventListener('pointerenter', measure);
window.addEventListener('resize', () => { box = null; });
field.addEventListener('pointermove', (e) => {
if (!box) measure();
const cx = box.left + box.width / 2, cy = box.top + box.height / 2;
const dx = e.clientX - cx, dy = e.clientY - cy;
const pull = Math.max(0, 1 - Math.hypot(dx, dy) / RADIUS);
if (pull <= 0) return;
fab.dataset.following = 'true';
fab.style.translate = (dx * pull * 0.34).toFixed(1) + 'px ' + (dy * pull * 0.34).toFixed(1) + 'px';
ico.style.translate = (dx * pull * 0.16).toFixed(1) + 'px ' + (dy * pull * 0.16).toFixed(1) + 'px';
});
field.addEventListener('pointerleave', () => {
fab.dataset.following = 'false';
fab.style.translate = '';
ico.style.translate = '';
box = null;
});
})();
```Here's a working CSS Floating 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: Magnetic Cursor Follower Floating Button CSS
Source: https://codefronts.com/components/css-floating-buttons/magnetic-hover-fab/
The award-site magnetic button, built properly: the button leans toward the pointer within a proximity field, the icon lags slightly behind the shell for parallax depth, and everything springs home on exit. Pointer-only by design — it detects fine pointers and hover capability and skips the whole effect on touch, where it would just be dead weight.
## HTML
```html
<section class="fb-23" aria-label="Magnetic cursor follower button demo">
<div class="fb-23__stage">
<p class="fb-23__eyebrow">proximity field · linear falloff</p>
<h2 class="fb-23__title">It leans toward you</h2>
<p class="fb-23__sub">Move the pointer near the button — not onto it. The shell follows at 34%, the icon at 16%, and the difference is what gives it depth.</p>
<div class="fb-23__field" id="fb-23-field">
<span class="fb-23__halo" aria-hidden="true"></span>
<button class="fb-23__fab" id="fb-23-fab" type="button" aria-label="Start a project">
<span class="fb-23__ico" id="fb-23-ico" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M5 12h14M13 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
</button>
</div>
<p class="fb-23__chip" id="fb-23-chip">pointer: fine · pull 0.34 / 0.16 · spring return 520ms</p>
</div>
</section>
```
## CSS
```css
.fb-23 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-23-bg);
--fb-23-bg: radial-gradient(90% 70% at 50% 42%,oklch(0.29 0.03 285),oklch(0.14 0.015 285) 70%);
--fb-23-accent: oklch(0.86 0.18 95);
--fb-23-ink: oklch(0.97 0.008 285);
--fb-23-mut: oklch(0.74 0.015 285);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-23-ink);
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
}
.fb-23 *,
.fb-23 *::before,
.fb-23 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-23__stage {
width: min(100%,58ch);
display: grid;
gap: clamp(16px,3vw,26px);
justify-items: center;
text-align: center;
}
.fb-23__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--fb-23-accent);
}
.fb-23__title {
font-size: clamp(27px,5vw,48px);
line-height: 1.04;
letter-spacing: -.035em;
font-weight: 700;
text-wrap: balance;
}
.fb-23__sub {
max-width: 48ch;
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-23-mut);
text-wrap: pretty;
}
.fb-23__field {
position: relative;
display: grid;
place-items: center;
width: min(88vw,340px);
height: min(38svh,240px);
border-radius: 28px;
}
.fb-23__halo {
position: absolute;
width: 230px;
height: 230px;
border-radius: 50%;
border: 1px dashed oklch(1 0 0/.14);
opacity: .7;
}
.fb-23__halo::after {
content: "";
position: absolute;
inset: 44px;
border-radius: 50%;
border: 1px dashed oklch(1 0 0/.1);
}
.fb-23__fab {
position: relative;
display: grid;
place-items: center;
inline-size: 96px;
block-size: 96px;
border: 0;
border-radius: 50%;
cursor: pointer;
color: oklch(0.17 0.04 95);
background: radial-gradient(120% 120% at 32% 26%,oklch(0.96 0.13 100),var(--fb-23-accent));
box-shadow: 0 22px 60px -18px oklch(0.86 0.18 95/.5),0 3px 10px oklch(0.08 0.01 285/.7);
transition: translate .52s cubic-bezier(.2,1.5,.35,1),scale .3s cubic-bezier(.2,1.4,.4,1),box-shadow .3s ease;
}
.fb-23__fab[data-following="true"] {
transition: scale .3s cubic-bezier(.2,1.4,.4,1),box-shadow .3s ease;
}
.fb-23__fab svg {
width: 32px;
height: 32px;
}
.fb-23__ico {
display: grid;
place-items: center;
transition: translate .52s cubic-bezier(.2,1.5,.35,1);
}
.fb-23__fab[data-following="true"] .fb-23__ico {
transition: none;
}
.fb-23__fab:hover {
scale: 1.06;
box-shadow: 0 30px 76px -18px oklch(0.86 0.18 95/.6),0 3px 10px oklch(0.08 0.01 285/.7);
}
.fb-23__fab:active {
scale: .95;
}
.fb-23__fab:focus-visible {
outline: 3px solid var(--fb-23-accent);
outline-offset: 8px;
}
.fb-23__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-23-mut);
padding: 8px 14px;
border: 1px solid oklch(1 0 0/.16);
border-radius: 99px;
}
@media (prefers-reduced-motion: reduce) {
.fb-23__fab,
.fb-23__ico {
transition: none !important;
}
}
```
## JavaScript
```js
(() => {
const field = document.getElementById('fb-23-field');
const fab = document.getElementById('fb-23-fab');
const ico = document.getElementById('fb-23-ico');
const chip = document.getElementById('fb-23-chip');
if (!field || !fab || fab.dataset.fbWired) return;
fab.dataset.fbWired = '1';
const fine = window.matchMedia('(hover:hover) and (pointer:fine)').matches;
const calm = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!fine || calm) {
chip.textContent = calm ? 'reduced motion — magnetism disabled' : 'coarse pointer — magnetism disabled on touch';
return;
}
const RADIUS = 165;
let box = null;
const measure = () => { box = fab.getBoundingClientRect(); };
field.addEventListener('pointerenter', measure);
window.addEventListener('resize', () => { box = null; });
field.addEventListener('pointermove', (e) => {
if (!box) measure();
const cx = box.left + box.width / 2, cy = box.top + box.height / 2;
const dx = e.clientX - cx, dy = e.clientY - cy;
const pull = Math.max(0, 1 - Math.hypot(dx, dy) / RADIUS);
if (pull <= 0) return;
fab.dataset.following = 'true';
fab.style.translate = (dx * pull * 0.34).toFixed(1) + 'px ' + (dy * pull * 0.34).toFixed(1) + 'px';
ico.style.translate = (dx * pull * 0.16).toFixed(1) + 'px ' + (dy * pull * 0.16).toFixed(1) + 'px';
});
field.addEventListener('pointerleave', () => {
fab.dataset.following = 'false';
fab.style.translate = '';
ico.style.translate = '';
box = null;
});
})();
```How this works
A single pointermove handler on a proximity zone, not on the button itself:
zone.addEventListener('pointermove', e => {
const r = btn.getBoundingClientRect();
const dx = e.clientX - (r.left + r.width/2), dy = e.clientY - (r.top + r.height/2);
const dist = Math.hypot(dx, dy);
const pull = Math.max(0, 1 - dist / RADIUS); // 0…1 falloff
btn.style.translate = dx * pull * .34 + 'px ' + dy * pull * .34 + 'px';
icon.style.translate = dx * pull * .16 + 'px ' + dy * pull * .16 + 'px';
});Listening on a larger zone is what creates the attraction: the button starts moving before the pointer reaches it. The linear falloff means the pull is strongest at the centre and fades to nothing at the field's edge, so there is no jump when the pointer crosses in or out.
The icon moving at roughly half the shell's rate is the parallax that gives the button apparent depth. Same principle as a layered hero image, at 20px scale.
Return is CSS, not JS: on pointerleave the inline translate is cleared and a spring transition takes over. Never animate the follow itself with a transition — it introduces lag between pointer and button and the magnetism dies. Follow instantly, spring back on release.
Make it yours
- Strength: the 0.34 multiplier is a comfortable lean. Above 0.5 the button outruns the pointer and feels slippery.
- Field size: a radius of ~2.2× the button's width reads as magnetic; much larger and the movement looks like random drift.
- Add rotation: a small
rotate(dx * pull * .02deg)makes the button tilt into the pull like a physical object. - Text ripple: for a labelled button, offset individual letters by decreasing amounts for a liquid text effect.
Gotchas — read before shipping
- Do not run this on touch. Detect with
matchMedia('(hover:hover) and (pointer:fine)')and bail out — on touch the button teleports on first tap. - Never put a CSS transition on the follow transform. The lag it introduces destroys the illusion; transition only the return.
- Reading getBoundingClientRect on every pointermove causes layout thrash. Cache it on pointerenter and invalidate on resize/scroll.
- A button that moves under the cursor can be impossible to click for users with motor impairments. Keep the offset small and always honour prefers-reduced-motion by disabling it entirely.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by oklch(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.
Pointer Events and the individual translate property are broadly supported (translate: Chrome 104+, Safari 14.1+, Firefox 72+). The hover/pointer media queries work everywhere.