13 / 15
All 15 in 15 Tailwind CSS Navigation Menus
- 01Tailwind CSS Navbar with Animated Hover Underline
- 02Tailwind CSS Dropdown Menu — Hover & Keyboard Accessible
- 03Tailwind CSS Mega Menu Navigation
- 04Tailwind CSS Hamburger Menu with Slide-Out Drawer
- 05Tailwind CSS Full-Screen Overlay Menu
- 06Tailwind CSS Sticky Navbar That Shrinks on Scroll
- 07Tailwind CSS Collapsible Sidebar Navigation
- 08Tailwind CSS Tabs with Animated Sliding Indicator
- 09Tailwind CSS Breadcrumb Navigation Examples
- 10Glassmorphism Navbar in Tailwind CSS
- 11Tailwind CSS Mobile Bottom Navigation Bar with Animated Icons
- 12Tailwind CSS Multi-Level Accordion Sidebar Menu
- 13Tailwind CSS Navbar with Magnetic Hover Effect
- 14Tailwind CSS Neon Glow Navigation Menu
- 15Tailwind CSS Animated Pill Navigation Indicator
Tailwind CSS Navbar with Magnetic Hover Effect
A creative-studio navbar whose links lean toward the cursor, with the label drifting further than its pill for a parallax feel, and a soft amber spotlight that follows the pointer across the page. JS only writes four custom properties; Tailwind consumes them with translate-x-(--tnv13-dx) and a radial-gradient background. Eased with a rAF lerp, disabled for touch and reduced motion.
Nucleo: 44,000+ SVG icons in five families · All Icons Bundle $149
Nucleo · Affiliate link
The code
<div class="tnv-13 relative isolate min-h-svh w-full overflow-hidden bg-tnv13-bg bg-[radial-gradient(520px_circle_at_var(--tnv13-mx,50%)_var(--tnv13-my,-30%),rgb(251_191_36/0.09),transparent_70%)] font-tnv13-sans text-tnv13-ink antialiased selection:bg-tnv13-acc selection:text-black">
<div aria-hidden="true" class="pointer-events-none absolute top-1/2 left-1/2 -z-10 size-[600px] -translate-1/2 rounded-full border border-white/[0.05]"></div>
<div aria-hidden="true" class="pointer-events-none absolute top-1/2 left-1/2 -z-10 size-[920px] -translate-1/2 rounded-full border border-white/[0.03]"></div>
<header class="fixed inset-x-0 top-0 z-20 flex items-center justify-between px-6 py-7 sm:px-12">
<a href="#" class="text-2xl font-bold tracking-[-0.04em]">Arp<span class="text-tnv13-acc">.</span></a>
<nav aria-label="Main" class="hidden md:block">
<ul class="flex items-center gap-2 text-[15px] font-medium text-tnv13-mut [&_a]:relative [&_a]:block [&_a]:rounded-xl [&_a]:px-4 [&_a]:py-2.5 [&_a]:[--tnv13-dx:0px] [&_a]:[--tnv13-dy:0px] [&_a]:translate-x-(--tnv13-dx) [&_a]:translate-y-(--tnv13-dy) [&_a]:transition-colors [&_a]:before:absolute [&_a]:before:inset-0 [&_a]:before:-z-10 [&_a]:before:scale-75 [&_a]:before:rounded-xl [&_a]:before:bg-white/[0.06] [&_a]:before:opacity-0 [&_a]:before:transition-[scale,opacity] [&_a]:before:duration-300 [&_a]:before:ease-[cubic-bezier(.34,1.56,.64,1)] [&_a]:before:content-[''] [&_a:hover]:text-white [&_a:hover]:before:scale-100 [&_a:hover]:before:opacity-100 [&_a:focus-visible]:text-white [&_a:focus-visible]:outline-2 [&_a:focus-visible]:outline-tnv13-acc [&_a[aria-current]]:text-white [&_span]:block [&_span]:translate-x-[calc(var(--tnv13-dx)*0.6)] [&_span]:translate-y-[calc(var(--tnv13-dy)*0.6)]">
<li><a href="#" data-magnet aria-current="page"><span>Work</span></a></li>
<li><a href="#" data-magnet><span>Studio</span></a></li>
<li><a href="#" data-magnet><span>Process</span></a></li>
<li><a href="#" data-magnet><span>Journal</span></a></li>
</ul>
</nav>
<a href="#" data-magnet class="inline-flex h-11 translate-x-(--tnv13-dx) translate-y-(--tnv13-dy) items-center gap-2 rounded-xl bg-tnv13-acc px-5 text-sm font-bold text-black transition-shadow [--tnv13-dx:0px] [--tnv13-dy:0px] hover:shadow-[0_0_36px_rgb(251_191_36/0.35)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tnv13-acc">Let's talk<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M7 17L17 7M8 7h9v9"/></svg></a>
</header>
<main class="grid min-h-svh place-items-center px-6 pt-32 pb-16 text-center [&_code]:rounded [&_code]:bg-white/[0.08] [&_code]:px-1.5 [&_code]:font-tnv13-mono [&_code]:text-[0.85em] [&_code]:text-tnv13-ink">
<div>
<p class="mb-6 font-tnv13-mono text-xs font-medium tracking-[0.2em] text-tnv13-acc uppercase">Creative direction & design</p>
<h1 class="text-[clamp(3rem,10vw,7.5rem)] leading-[0.92] font-bold tracking-[-0.055em]">Move the cursor<span class="sr-only"> near the nav</span><br><span aria-hidden="true" class="text-transparent [-webkit-text-stroke:1px_rgb(255_255_255/0.3)]">near the nav</span></h1>
<p class="mx-auto mt-7 max-w-md text-base/[1.7] text-tnv13-mut text-pretty">Links lean toward the pointer while their labels drift further, and a spotlight follows you across the page. JS writes <code>--tnv13-dx</code>; Tailwind reads it with <code>translate-x-(--tnv13-dx)</code>.</p>
</div>
</main>
</div><div class="tnv-13 relative isolate min-h-svh w-full overflow-hidden bg-tnv13-bg bg-[radial-gradient(520px_circle_at_var(--tnv13-mx,50%)_var(--tnv13-my,-30%),rgb(251_191_36/0.09),transparent_70%)] font-tnv13-sans text-tnv13-ink antialiased selection:bg-tnv13-acc selection:text-black">
<div aria-hidden="true" class="pointer-events-none absolute top-1/2 left-1/2 -z-10 size-[600px] -translate-1/2 rounded-full border border-white/[0.05]"></div>
<div aria-hidden="true" class="pointer-events-none absolute top-1/2 left-1/2 -z-10 size-[920px] -translate-1/2 rounded-full border border-white/[0.03]"></div>
<header class="fixed inset-x-0 top-0 z-20 flex items-center justify-between px-6 py-7 sm:px-12">
<a href="#" class="text-2xl font-bold tracking-[-0.04em]">Arp<span class="text-tnv13-acc">.</span></a>
<nav aria-label="Main" class="hidden md:block">
<ul class="flex items-center gap-2 text-[15px] font-medium text-tnv13-mut [&_a]:relative [&_a]:block [&_a]:rounded-xl [&_a]:px-4 [&_a]:py-2.5 [&_a]:[--tnv13-dx:0px] [&_a]:[--tnv13-dy:0px] [&_a]:translate-x-(--tnv13-dx) [&_a]:translate-y-(--tnv13-dy) [&_a]:transition-colors [&_a]:before:absolute [&_a]:before:inset-0 [&_a]:before:-z-10 [&_a]:before:scale-75 [&_a]:before:rounded-xl [&_a]:before:bg-white/[0.06] [&_a]:before:opacity-0 [&_a]:before:transition-[scale,opacity] [&_a]:before:duration-300 [&_a]:before:ease-[cubic-bezier(.34,1.56,.64,1)] [&_a]:before:content-[''] [&_a:hover]:text-white [&_a:hover]:before:scale-100 [&_a:hover]:before:opacity-100 [&_a:focus-visible]:text-white [&_a:focus-visible]:outline-2 [&_a:focus-visible]:outline-tnv13-acc [&_a[aria-current]]:text-white [&_span]:block [&_span]:translate-x-[calc(var(--tnv13-dx)*0.6)] [&_span]:translate-y-[calc(var(--tnv13-dy)*0.6)]">
<li><a href="#" data-magnet aria-current="page"><span>Work</span></a></li>
<li><a href="#" data-magnet><span>Studio</span></a></li>
<li><a href="#" data-magnet><span>Process</span></a></li>
<li><a href="#" data-magnet><span>Journal</span></a></li>
</ul>
</nav>
<a href="#" data-magnet class="inline-flex h-11 translate-x-(--tnv13-dx) translate-y-(--tnv13-dy) items-center gap-2 rounded-xl bg-tnv13-acc px-5 text-sm font-bold text-black transition-shadow [--tnv13-dx:0px] [--tnv13-dy:0px] hover:shadow-[0_0_36px_rgb(251_191_36/0.35)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tnv13-acc">Let's talk<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M7 17L17 7M8 7h9v9"/></svg></a>
</header>
<main class="grid min-h-svh place-items-center px-6 pt-32 pb-16 text-center [&_code]:rounded [&_code]:bg-white/[0.08] [&_code]:px-1.5 [&_code]:font-tnv13-mono [&_code]:text-[0.85em] [&_code]:text-tnv13-ink">
<div>
<p class="mb-6 font-tnv13-mono text-xs font-medium tracking-[0.2em] text-tnv13-acc uppercase">Creative direction & design</p>
<h1 class="text-[clamp(3rem,10vw,7.5rem)] leading-[0.92] font-bold tracking-[-0.055em]">Move the cursor<span class="sr-only"> near the nav</span><br><span aria-hidden="true" class="text-transparent [-webkit-text-stroke:1px_rgb(255_255_255/0.3)]">near the nav</span></h1>
<p class="mx-auto mt-7 max-w-md text-base/[1.7] text-tnv13-mut text-pretty">Links lean toward the pointer while their labels drift further, and a spotlight follows you across the page. JS writes <code>--tnv13-dx</code>; Tailwind reads it with <code>translate-x-(--tnv13-dx)</code>.</p>
</div>
</main>
</div>@import "tailwindcss";
@theme {
--color-tnv13-bg: #070707;
--color-tnv13-bg: oklch(0.12 0 0);
--color-tnv13-ink: #fafafa;
--color-tnv13-ink: oklch(0.985 0 0);
--color-tnv13-mut: #a3a3a3;
--color-tnv13-mut: oklch(0.708 0 0);
--color-tnv13-acc: #fbbf24;
--color-tnv13-acc: oklch(0.828 0.189 84);
--font-tnv13-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tnv13-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tnv-13 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tnv13-bg: #070707;
--color-tnv13-bg: oklch(0.12 0 0);
--color-tnv13-ink: #fafafa;
--color-tnv13-ink: oklch(0.985 0 0);
--color-tnv13-mut: #a3a3a3;
--color-tnv13-mut: oklch(0.708 0 0);
--color-tnv13-acc: #fbbf24;
--color-tnv13-acc: oklch(0.828 0.189 84);
--font-tnv13-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tnv13-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tnv-13 {
width: 100%;
min-height: 100svh;
}(() => {
const root = document.querySelector('.tnv-13');
if (!root || root.dataset.wired) return;
root.dataset.wired = '1';
const magnetic = matchMedia('(pointer: fine)').matches && !matchMedia('(prefers-reduced-motion: reduce)').matches;
const els = [...root.querySelectorAll('[data-magnet]')];
const st = els.map(() => ({ x: 0, y: 0 }));
const RADIUS = 90, PULL = 0.3, MAX = 14, EASE = 0.18;
const clamp = (v) => Math.max(-MAX, Math.min(MAX, v));
let px = -1e4, py = -1e4, raf = 0;
const frame = () => {
raf = 0;
let busy = false;
els.forEach((el, i) => {
const s = st[i];
const r = el.getBoundingClientRect();
const cx = r.left - s.x + r.width / 2, cy = r.top - s.y + r.height / 2;
const dx = px - cx, dy = py - cy;
const near = Math.hypot(dx, dy) < RADIUS + r.width / 2;
const tx = near ? clamp(dx * PULL) : 0, ty = near ? clamp(dy * PULL) : 0;
s.x += (tx - s.x) * EASE;
s.y += (ty - s.y) * EASE;
if (Math.abs(tx - s.x) > 0.05 || Math.abs(ty - s.y) > 0.05) busy = true;
el.style.setProperty('--tnv13-dx', s.x.toFixed(2) + 'px');
el.style.setProperty('--tnv13-dy', s.y.toFixed(2) + 'px');
});
if (busy) raf = requestAnimationFrame(frame);
};
const kick = () => { if (!raf) raf = requestAnimationFrame(frame); };
root.addEventListener('pointermove', (e) => {
const b = root.getBoundingClientRect();
root.style.setProperty('--tnv13-mx', (e.clientX - b.left) + 'px');
root.style.setProperty('--tnv13-my', (e.clientY - b.top) + 'px');
if (magnetic) { px = e.clientX; py = e.clientY; kick(); }
}, { passive: true });
root.addEventListener('pointerleave', () => { px = py = -1e4; kick(); });
})();(() => {
const root = document.querySelector('.tnv-13');
if (!root || root.dataset.wired) return;
root.dataset.wired = '1';
const magnetic = matchMedia('(pointer: fine)').matches && !matchMedia('(prefers-reduced-motion: reduce)').matches;
const els = [...root.querySelectorAll('[data-magnet]')];
const st = els.map(() => ({ x: 0, y: 0 }));
const RADIUS = 90, PULL = 0.3, MAX = 14, EASE = 0.18;
const clamp = (v) => Math.max(-MAX, Math.min(MAX, v));
let px = -1e4, py = -1e4, raf = 0;
const frame = () => {
raf = 0;
let busy = false;
els.forEach((el, i) => {
const s = st[i];
const r = el.getBoundingClientRect();
const cx = r.left - s.x + r.width / 2, cy = r.top - s.y + r.height / 2;
const dx = px - cx, dy = py - cy;
const near = Math.hypot(dx, dy) < RADIUS + r.width / 2;
const tx = near ? clamp(dx * PULL) : 0, ty = near ? clamp(dy * PULL) : 0;
s.x += (tx - s.x) * EASE;
s.y += (ty - s.y) * EASE;
if (Math.abs(tx - s.x) > 0.05 || Math.abs(ty - s.y) > 0.05) busy = true;
el.style.setProperty('--tnv13-dx', s.x.toFixed(2) + 'px');
el.style.setProperty('--tnv13-dy', s.y.toFixed(2) + 'px');
});
if (busy) raf = requestAnimationFrame(frame);
};
const kick = () => { if (!raf) raf = requestAnimationFrame(frame); };
root.addEventListener('pointermove', (e) => {
const b = root.getBoundingClientRect();
root.style.setProperty('--tnv13-mx', (e.clientX - b.left) + 'px');
root.style.setProperty('--tnv13-my', (e.clientY - b.top) + 'px');
if (magnetic) { px = e.clientX; py = e.clientY; kick(); }
}, { passive: true });
root.addEventListener('pointerleave', () => { px = py = -1e4; kick(); });
})();Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tnv13-bg: #070707;
--color-tnv13-bg: oklch(0.12 0 0);
--color-tnv13-ink: #fafafa;
--color-tnv13-ink: oklch(0.985 0 0);
--color-tnv13-mut: #a3a3a3;
--color-tnv13-mut: oklch(0.708 0 0);
--color-tnv13-acc: #fbbf24;
--color-tnv13-acc: oklch(0.828 0.189 84);
--font-tnv13-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tnv13-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tnv-13 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tnv13-bg: #070707;
--color-tnv13-bg: oklch(0.12 0 0);
--color-tnv13-ink: #fafafa;
--color-tnv13-ink: oklch(0.985 0 0);
--color-tnv13-mut: #a3a3a3;
--color-tnv13-mut: oklch(0.708 0 0);
--color-tnv13-acc: #fbbf24;
--color-tnv13-acc: oklch(0.828 0.189 84);
--font-tnv13-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tnv13-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tnv-13 {
width: 100%;
min-height: 100svh;
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&family=JetBrains+Mono:wght@400;500&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&family=JetBrains+Mono:wght@400;500&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The
.nav-13__links a{transform:translate(var(--dx),var(--dy))}rule becametranslate-x-(--tnv13-dx); the separate fixed glow<div>became a radial-gradient on the root reading--tnv13-mx/my. - Shared link styles (hover pill, parallax label) are declared once on the
<ul>. - Upgrade from the CSS original: its handler snapped offsets every
mousemoveand measured transformed boxes (a feedback loop); it now eases in a rAF loop that sleeps when idle and measures untransformed centres. Label parallax is new. - Bug & accessibility fixes: the decorative rings were never positioned and sat in the corner; they're centred now. The outlined hero line is
aria-hiddenwith ansr-onlytwin, muted copy rose to 8.5:1, and the effect turns off for touch and reduced motion.
Here's a working CSS demo 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: Tailwind CSS Navbar with Magnetic Hover Effect
Source: https://codefronts.com/navigation/tailwind-css-navigation-menus/tailwind-css-navbar-magnetic-hover-effect/
A creative-studio navbar whose links lean toward the cursor, with the label drifting further than its pill for a parallax feel, and a soft amber spotlight that follows the pointer across the page. JS only writes four custom properties; Tailwind consumes them with translate-x-(--tnv13-dx) and a radial-gradient background. Eased with a rAF lerp, disabled for touch and reduced motion.
## HTML
```html
<div class="tnv-13 relative isolate min-h-svh w-full overflow-hidden bg-tnv13-bg bg-[radial-gradient(520px_circle_at_var(--tnv13-mx,50%)_var(--tnv13-my,-30%),rgb(251_191_36/0.09),transparent_70%)] font-tnv13-sans text-tnv13-ink antialiased selection:bg-tnv13-acc selection:text-black">
<div aria-hidden="true" class="pointer-events-none absolute top-1/2 left-1/2 -z-10 size-[600px] -translate-1/2 rounded-full border border-white/[0.05]"></div>
<div aria-hidden="true" class="pointer-events-none absolute top-1/2 left-1/2 -z-10 size-[920px] -translate-1/2 rounded-full border border-white/[0.03]"></div>
<header class="fixed inset-x-0 top-0 z-20 flex items-center justify-between px-6 py-7 sm:px-12">
<a href="#" class="text-2xl font-bold tracking-[-0.04em]">Arp<span class="text-tnv13-acc">.</span></a>
<nav aria-label="Main" class="hidden md:block">
<ul class="flex items-center gap-2 text-[15px] font-medium text-tnv13-mut [&_a]:relative [&_a]:block [&_a]:rounded-xl [&_a]:px-4 [&_a]:py-2.5 [&_a]:[--tnv13-dx:0px] [&_a]:[--tnv13-dy:0px] [&_a]:translate-x-(--tnv13-dx) [&_a]:translate-y-(--tnv13-dy) [&_a]:transition-colors [&_a]:before:absolute [&_a]:before:inset-0 [&_a]:before:-z-10 [&_a]:before:scale-75 [&_a]:before:rounded-xl [&_a]:before:bg-white/[0.06] [&_a]:before:opacity-0 [&_a]:before:transition-[scale,opacity] [&_a]:before:duration-300 [&_a]:before:ease-[cubic-bezier(.34,1.56,.64,1)] [&_a]:before:content-[''] [&_a:hover]:text-white [&_a:hover]:before:scale-100 [&_a:hover]:before:opacity-100 [&_a:focus-visible]:text-white [&_a:focus-visible]:outline-2 [&_a:focus-visible]:outline-tnv13-acc [&_a[aria-current]]:text-white [&_span]:block [&_span]:translate-x-[calc(var(--tnv13-dx)*0.6)] [&_span]:translate-y-[calc(var(--tnv13-dy)*0.6)]">
<li><a href="#" data-magnet aria-current="page"><span>Work</span></a></li>
<li><a href="#" data-magnet><span>Studio</span></a></li>
<li><a href="#" data-magnet><span>Process</span></a></li>
<li><a href="#" data-magnet><span>Journal</span></a></li>
</ul>
</nav>
<a href="#" data-magnet class="inline-flex h-11 translate-x-(--tnv13-dx) translate-y-(--tnv13-dy) items-center gap-2 rounded-xl bg-tnv13-acc px-5 text-sm font-bold text-black transition-shadow [--tnv13-dx:0px] [--tnv13-dy:0px] hover:shadow-[0_0_36px_rgb(251_191_36/0.35)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tnv13-acc">Let's talk<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M7 17L17 7M8 7h9v9"/></svg></a>
</header>
<main class="grid min-h-svh place-items-center px-6 pt-32 pb-16 text-center [&_code]:rounded [&_code]:bg-white/[0.08] [&_code]:px-1.5 [&_code]:font-tnv13-mono [&_code]:text-[0.85em] [&_code]:text-tnv13-ink">
<div>
<p class="mb-6 font-tnv13-mono text-xs font-medium tracking-[0.2em] text-tnv13-acc uppercase">Creative direction & design</p>
<h1 class="text-[clamp(3rem,10vw,7.5rem)] leading-[0.92] font-bold tracking-[-0.055em]">Move the cursor<span class="sr-only"> near the nav</span><br><span aria-hidden="true" class="text-transparent [-webkit-text-stroke:1px_rgb(255_255_255/0.3)]">near the nav</span></h1>
<p class="mx-auto mt-7 max-w-md text-base/[1.7] text-tnv13-mut text-pretty">Links lean toward the pointer while their labels drift further, and a spotlight follows you across the page. JS writes <code>--tnv13-dx</code>; Tailwind reads it with <code>translate-x-(--tnv13-dx)</code>.</p>
</div>
</main>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tnv13-bg: #070707;
--color-tnv13-bg: oklch(0.12 0 0);
--color-tnv13-ink: #fafafa;
--color-tnv13-ink: oklch(0.985 0 0);
--color-tnv13-mut: #a3a3a3;
--color-tnv13-mut: oklch(0.708 0 0);
--color-tnv13-acc: #fbbf24;
--color-tnv13-acc: oklch(0.828 0.189 84);
--font-tnv13-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tnv13-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tnv-13 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.tnv-13');
if (!root || root.dataset.wired) return;
root.dataset.wired = '1';
const magnetic = matchMedia('(pointer: fine)').matches && !matchMedia('(prefers-reduced-motion: reduce)').matches;
const els = [...root.querySelectorAll('[data-magnet]')];
const st = els.map(() => ({ x: 0, y: 0 }));
const RADIUS = 90, PULL = 0.3, MAX = 14, EASE = 0.18;
const clamp = (v) => Math.max(-MAX, Math.min(MAX, v));
let px = -1e4, py = -1e4, raf = 0;
const frame = () => {
raf = 0;
let busy = false;
els.forEach((el, i) => {
const s = st[i];
const r = el.getBoundingClientRect();
const cx = r.left - s.x + r.width / 2, cy = r.top - s.y + r.height / 2;
const dx = px - cx, dy = py - cy;
const near = Math.hypot(dx, dy) < RADIUS + r.width / 2;
const tx = near ? clamp(dx * PULL) : 0, ty = near ? clamp(dy * PULL) : 0;
s.x += (tx - s.x) * EASE;
s.y += (ty - s.y) * EASE;
if (Math.abs(tx - s.x) > 0.05 || Math.abs(ty - s.y) > 0.05) busy = true;
el.style.setProperty('--tnv13-dx', s.x.toFixed(2) + 'px');
el.style.setProperty('--tnv13-dy', s.y.toFixed(2) + 'px');
});
if (busy) raf = requestAnimationFrame(frame);
};
const kick = () => { if (!raf) raf = requestAnimationFrame(frame); };
root.addEventListener('pointermove', (e) => {
const b = root.getBoundingClientRect();
root.style.setProperty('--tnv13-mx', (e.clientX - b.left) + 'px');
root.style.setProperty('--tnv13-my', (e.clientY - b.top) + 'px');
if (magnetic) { px = e.clientX; py = e.clientY; kick(); }
}, { passive: true });
root.addEventListener('pointerleave', () => { px = py = -1e4; kick(); });
})();
```Here's a working CSS demo 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: Tailwind CSS Navbar with Magnetic Hover Effect
Source: https://codefronts.com/navigation/tailwind-css-navigation-menus/tailwind-css-navbar-magnetic-hover-effect/
A creative-studio navbar whose links lean toward the cursor, with the label drifting further than its pill for a parallax feel, and a soft amber spotlight that follows the pointer across the page. JS only writes four custom properties; Tailwind consumes them with translate-x-(--tnv13-dx) and a radial-gradient background. Eased with a rAF lerp, disabled for touch and reduced motion.
## HTML
```html
<div class="tnv-13 relative isolate min-h-svh w-full overflow-hidden bg-tnv13-bg bg-[radial-gradient(520px_circle_at_var(--tnv13-mx,50%)_var(--tnv13-my,-30%),rgb(251_191_36/0.09),transparent_70%)] font-tnv13-sans text-tnv13-ink antialiased selection:bg-tnv13-acc selection:text-black">
<div aria-hidden="true" class="pointer-events-none absolute top-1/2 left-1/2 -z-10 size-[600px] -translate-1/2 rounded-full border border-white/[0.05]"></div>
<div aria-hidden="true" class="pointer-events-none absolute top-1/2 left-1/2 -z-10 size-[920px] -translate-1/2 rounded-full border border-white/[0.03]"></div>
<header class="fixed inset-x-0 top-0 z-20 flex items-center justify-between px-6 py-7 sm:px-12">
<a href="#" class="text-2xl font-bold tracking-[-0.04em]">Arp<span class="text-tnv13-acc">.</span></a>
<nav aria-label="Main" class="hidden md:block">
<ul class="flex items-center gap-2 text-[15px] font-medium text-tnv13-mut [&_a]:relative [&_a]:block [&_a]:rounded-xl [&_a]:px-4 [&_a]:py-2.5 [&_a]:[--tnv13-dx:0px] [&_a]:[--tnv13-dy:0px] [&_a]:translate-x-(--tnv13-dx) [&_a]:translate-y-(--tnv13-dy) [&_a]:transition-colors [&_a]:before:absolute [&_a]:before:inset-0 [&_a]:before:-z-10 [&_a]:before:scale-75 [&_a]:before:rounded-xl [&_a]:before:bg-white/[0.06] [&_a]:before:opacity-0 [&_a]:before:transition-[scale,opacity] [&_a]:before:duration-300 [&_a]:before:ease-[cubic-bezier(.34,1.56,.64,1)] [&_a]:before:content-[''] [&_a:hover]:text-white [&_a:hover]:before:scale-100 [&_a:hover]:before:opacity-100 [&_a:focus-visible]:text-white [&_a:focus-visible]:outline-2 [&_a:focus-visible]:outline-tnv13-acc [&_a[aria-current]]:text-white [&_span]:block [&_span]:translate-x-[calc(var(--tnv13-dx)*0.6)] [&_span]:translate-y-[calc(var(--tnv13-dy)*0.6)]">
<li><a href="#" data-magnet aria-current="page"><span>Work</span></a></li>
<li><a href="#" data-magnet><span>Studio</span></a></li>
<li><a href="#" data-magnet><span>Process</span></a></li>
<li><a href="#" data-magnet><span>Journal</span></a></li>
</ul>
</nav>
<a href="#" data-magnet class="inline-flex h-11 translate-x-(--tnv13-dx) translate-y-(--tnv13-dy) items-center gap-2 rounded-xl bg-tnv13-acc px-5 text-sm font-bold text-black transition-shadow [--tnv13-dx:0px] [--tnv13-dy:0px] hover:shadow-[0_0_36px_rgb(251_191_36/0.35)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tnv13-acc">Let's talk<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M7 17L17 7M8 7h9v9"/></svg></a>
</header>
<main class="grid min-h-svh place-items-center px-6 pt-32 pb-16 text-center [&_code]:rounded [&_code]:bg-white/[0.08] [&_code]:px-1.5 [&_code]:font-tnv13-mono [&_code]:text-[0.85em] [&_code]:text-tnv13-ink">
<div>
<p class="mb-6 font-tnv13-mono text-xs font-medium tracking-[0.2em] text-tnv13-acc uppercase">Creative direction & design</p>
<h1 class="text-[clamp(3rem,10vw,7.5rem)] leading-[0.92] font-bold tracking-[-0.055em]">Move the cursor<span class="sr-only"> near the nav</span><br><span aria-hidden="true" class="text-transparent [-webkit-text-stroke:1px_rgb(255_255_255/0.3)]">near the nav</span></h1>
<p class="mx-auto mt-7 max-w-md text-base/[1.7] text-tnv13-mut text-pretty">Links lean toward the pointer while their labels drift further, and a spotlight follows you across the page. JS writes <code>--tnv13-dx</code>; Tailwind reads it with <code>translate-x-(--tnv13-dx)</code>.</p>
</div>
</main>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tnv13-bg: #070707;
--color-tnv13-bg: oklch(0.12 0 0);
--color-tnv13-ink: #fafafa;
--color-tnv13-ink: oklch(0.985 0 0);
--color-tnv13-mut: #a3a3a3;
--color-tnv13-mut: oklch(0.708 0 0);
--color-tnv13-acc: #fbbf24;
--color-tnv13-acc: oklch(0.828 0.189 84);
--font-tnv13-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tnv13-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tnv-13 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.tnv-13');
if (!root || root.dataset.wired) return;
root.dataset.wired = '1';
const magnetic = matchMedia('(pointer: fine)').matches && !matchMedia('(prefers-reduced-motion: reduce)').matches;
const els = [...root.querySelectorAll('[data-magnet]')];
const st = els.map(() => ({ x: 0, y: 0 }));
const RADIUS = 90, PULL = 0.3, MAX = 14, EASE = 0.18;
const clamp = (v) => Math.max(-MAX, Math.min(MAX, v));
let px = -1e4, py = -1e4, raf = 0;
const frame = () => {
raf = 0;
let busy = false;
els.forEach((el, i) => {
const s = st[i];
const r = el.getBoundingClientRect();
const cx = r.left - s.x + r.width / 2, cy = r.top - s.y + r.height / 2;
const dx = px - cx, dy = py - cy;
const near = Math.hypot(dx, dy) < RADIUS + r.width / 2;
const tx = near ? clamp(dx * PULL) : 0, ty = near ? clamp(dy * PULL) : 0;
s.x += (tx - s.x) * EASE;
s.y += (ty - s.y) * EASE;
if (Math.abs(tx - s.x) > 0.05 || Math.abs(ty - s.y) > 0.05) busy = true;
el.style.setProperty('--tnv13-dx', s.x.toFixed(2) + 'px');
el.style.setProperty('--tnv13-dy', s.y.toFixed(2) + 'px');
});
if (busy) raf = requestAnimationFrame(frame);
};
const kick = () => { if (!raf) raf = requestAnimationFrame(frame); };
root.addEventListener('pointermove', (e) => {
const b = root.getBoundingClientRect();
root.style.setProperty('--tnv13-mx', (e.clientX - b.left) + 'px');
root.style.setProperty('--tnv13-my', (e.clientY - b.top) + 'px');
if (magnetic) { px = e.clientX; py = e.clientY; kick(); }
}, { passive: true });
root.addEventListener('pointerleave', () => { px = py = -1e4; kick(); });
})();
```How this works
JS writes numbers, Tailwind draws. The controller never sets a transform; it sets --tnv13-dx/--tnv13-dy on each magnetic element and --tnv13-mx/--tnv13-my on the root:
<a data-magnet class="translate-x-(--tnv13-dx) translate-y-(--tnv13-dy)">
<span class="translate-x-[calc(var(--tnv13-dx)*0.6)] …">Work</span>
</a>Smooth without CSS transitions. Each frame moves the offset 18% closer to its target, which gives a weighty follow and a springy release. The loop stops once everything settles, so an idle page costs nothing. Positions are measured minus the current offset, so the pull never feeds back on itself.
Make it yours
- Stronger pull: raise
PULL(0.3) orMAX(14px) in the script. - Lazier follow: lower the lerp factor from
0.18to0.1. - Spotlight colour: edit the
rgb(251_191_36/0.09)stop in the root background.
Gotchas — read before shipping
- Keep the pull capped; values over ~20px let links collide with their neighbours.
- The effect is skipped on
(pointer: coarse)devices and under reduced motion; links stay perfectly usable. - Transparent outlined text (the hero's second line) must stay decorative; the real heading text is the solid line plus an
sr-onlycopy.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Pointer Events and individual translate are universal in this range.