31 / 31
All 31 in 31 Tailwind CSS Hero Sections
- 01Centered Hero Section in Tailwind CSS with Serif Headline & Dual CTA
- 02Tailwind CSS Split Hero Section with Dashboard Mockup
- 03Tailwind CSS Animated Gradient Mesh Hero Section
- 04Glassmorphism Hero Section in Tailwind CSS with Aurora Background
- 05Tailwind CSS Developer Hero Section with Terminal Window
- 06Tailwind Hero Section with Animated Geometric Shapes Background
- 07Tailwind CSS Hero Section with Star Rating & Logo Cloud
- 08Tailwind CSS Animated Headline Hero with Staggered Word Reveal
- 09Before and After Image Comparison Slider Hero in Tailwind CSS
- 10Full-Screen Tailwind Hero with Animated Scroll Down Indicator
- 11Magazine Cover Hero Section in Tailwind CSS
- 12Asymmetric Typography Hero Section in Tailwind CSS
- 13Tailwind CSS Hero Section with Oversized Background Number
- 14Infinite Scrolling Marquee Hero Section in Tailwind CSS
- 15Tailwind CSS Hero Section with Image & Overlay Card
- 16Brutalist Hero Section in Tailwind CSS
- 17Vertical Text Hero Section in Tailwind CSS (Japanese Writing Mode)
- 18Kinetic Typography Hero in Tailwind CSS with Animated Letters
- 19Bento Grid Hero Section in Tailwind CSS
- 20Animated Gradient Text Hero Section in Tailwind CSS
- 21Tailwind CSS Product Hero with Floating Feature Badges
- 22Tailwind CSS Hero Section with Pricing Card
- 23Mobile App Landing Page Hero in Tailwind CSS with App Store Buttons
- 24Neon Glow Hero Section in Tailwind CSS (Cyberpunk)
- 25Parallax Mountain Landscape Hero in Tailwind CSS
- 26Isometric Illustration Hero Section in Tailwind CSS
- 273D Card Stack Hero in Tailwind CSS with Hover Fan-Out
- 28Animated Starfield Background Hero in Tailwind CSS
- 29Glitch Text Effect Hero Section in Tailwind CSS
- 30Animated Conic Gradient Ring Hero in Tailwind CSS
- 31Mouse-Tracking Liquid Glass Lens Hero in Tailwind CSS
Mouse-Tracking Liquid Glass Lens Hero in Tailwind CSS
A Liquid Glass lens that follows the pointer across a shifting conic-mesh hero, refracting the headline and colours beneath it with an eased spring; on touch it drifts in a slow figure-eight until you drag. Now with real CTAs and a reduced-motion path.
Complete Tailwind CSS landing page templates in HTML, React, Next.js and Vue · from $49
Cruip · Affiliate link
The code
<section data-ths31-hero style="--mx:50%;--my:50%" class="ths-31 relative isolate grid w-full min-h-svh cursor-crosshair touch-pan-y content-center overflow-hidden bg-[conic-gradient(from_60deg_at_30%_30%,var(--color-ths31-amber),var(--color-ths31-red),var(--color-ths31-violet),var(--color-ths31-cyan),var(--color-ths31-amber))] bg-size-[200%_200%] px-6 py-16 font-ths31-sans text-white animate-ths31-mesh motion-reduce:animate-none sm:px-12" aria-label="Mouse-tracking liquid glass hero">
<div aria-hidden="true" class="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(70%_60%_at_20%_60%,rgb(0_0_0/0.28),transparent_70%)]"></div>
<div class="max-w-3xl">
<p class="text-[13px] font-semibold tracking-[0.14em] uppercase [text-shadow:0_1px_8px_rgb(0_0_0/0.3)]">Frontend that feels physical</p>
<h1 class="mt-3 font-ths31-display text-[clamp(64px,14vw,150px)] leading-[0.88] font-extrabold tracking-[-0.045em] [text-shadow:0_4px_24px_rgb(0_0_0/0.28)]">Liquid<br>Glass</h1>
<p class="mt-5 max-w-[34ch] text-base/relaxed [text-shadow:0_1px_8px_rgb(0_0_0/0.35)]">Move — or tap and drag on touch — the lens refracts everything beneath it.</p>
<div class="mt-8 flex flex-wrap gap-3">
<a href="#work" class="inline-flex min-h-12 cursor-pointer items-center rounded-full bg-white px-7 text-[15px] font-semibold text-ths31-ink shadow-[0_12px_30px_-10px_rgb(0_0_0/0.45)] transition hover:-translate-y-px focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white motion-reduce:transition-none">See the work</a>
<a href="#source" class="inline-flex min-h-12 cursor-pointer items-center rounded-full border border-white/60 bg-white/10 px-6 text-[15px] font-semibold text-white backdrop-blur-sm transition hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white">View source</a>
</div>
</div>
<span aria-hidden="true" data-ths31-lens class="pointer-events-none absolute top-(--my) left-(--mx) size-48 -translate-1/2 rounded-full border-2 border-white/70 bg-[radial-gradient(circle_at_35%_30%,rgb(255_255_255/0.32),rgb(255_255_255/0.05)_55%,transparent_72%)] shadow-[inset_0_3px_12px_rgb(255_255_255/0.55),inset_0_-12px_26px_rgb(0_0_0/0.28),0_14px_40px_-8px_rgb(0_0_0/0.55)] backdrop-blur-md backdrop-contrast-110 backdrop-saturate-200 motion-reduce:hidden"></span>
</section><section data-ths31-hero style="--mx:50%;--my:50%" class="ths-31 relative isolate grid w-full min-h-svh cursor-crosshair touch-pan-y content-center overflow-hidden bg-[conic-gradient(from_60deg_at_30%_30%,var(--color-ths31-amber),var(--color-ths31-red),var(--color-ths31-violet),var(--color-ths31-cyan),var(--color-ths31-amber))] bg-size-[200%_200%] px-6 py-16 font-ths31-sans text-white animate-ths31-mesh motion-reduce:animate-none sm:px-12" aria-label="Mouse-tracking liquid glass hero">
<div aria-hidden="true" class="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(70%_60%_at_20%_60%,rgb(0_0_0/0.28),transparent_70%)]"></div>
<div class="max-w-3xl">
<p class="text-[13px] font-semibold tracking-[0.14em] uppercase [text-shadow:0_1px_8px_rgb(0_0_0/0.3)]">Frontend that feels physical</p>
<h1 class="mt-3 font-ths31-display text-[clamp(64px,14vw,150px)] leading-[0.88] font-extrabold tracking-[-0.045em] [text-shadow:0_4px_24px_rgb(0_0_0/0.28)]">Liquid<br>Glass</h1>
<p class="mt-5 max-w-[34ch] text-base/relaxed [text-shadow:0_1px_8px_rgb(0_0_0/0.35)]">Move — or tap and drag on touch — the lens refracts everything beneath it.</p>
<div class="mt-8 flex flex-wrap gap-3">
<a href="#work" class="inline-flex min-h-12 cursor-pointer items-center rounded-full bg-white px-7 text-[15px] font-semibold text-ths31-ink shadow-[0_12px_30px_-10px_rgb(0_0_0/0.45)] transition hover:-translate-y-px focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white motion-reduce:transition-none">See the work</a>
<a href="#source" class="inline-flex min-h-12 cursor-pointer items-center rounded-full border border-white/60 bg-white/10 px-6 text-[15px] font-semibold text-white backdrop-blur-sm transition hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white">View source</a>
</div>
</div>
<span aria-hidden="true" data-ths31-lens class="pointer-events-none absolute top-(--my) left-(--mx) size-48 -translate-1/2 rounded-full border-2 border-white/70 bg-[radial-gradient(circle_at_35%_30%,rgb(255_255_255/0.32),rgb(255_255_255/0.05)_55%,transparent_72%)] shadow-[inset_0_3px_12px_rgb(255_255_255/0.55),inset_0_-12px_26px_rgb(0_0_0/0.28),0_14px_40px_-8px_rgb(0_0_0/0.55)] backdrop-blur-md backdrop-contrast-110 backdrop-saturate-200 motion-reduce:hidden"></span>
</section>@import "tailwindcss";
@theme {
--color-ths31-ink: #1c1030;
--color-ths31-ink: oklch(0.209 0.062 298.2);
--color-ths31-amber: #f59e0b;
--color-ths31-amber: oklch(0.769 0.165 70.1);
--color-ths31-red: #ef4444;
--color-ths31-red: oklch(0.637 0.208 25.3);
--color-ths31-violet: #8b5cf6;
--color-ths31-violet: oklch(0.606 0.219 292.7);
--color-ths31-cyan: #06b6d4;
--color-ths31-cyan: oklch(0.715 0.126 215.2);
--font-ths31-display: 'Bricolage Grotesque', system-ui, sans-serif;
--font-ths31-sans: 'Geist', system-ui, sans-serif;
--animate-ths31-mesh: ths31-mesh 16s ease-in-out infinite;
@keyframes ths31-mesh {
50% {
background-position: 100% 100%;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.ths-31 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-ths31-ink: #1c1030;
--color-ths31-ink: oklch(0.209 0.062 298.2);
--color-ths31-amber: #f59e0b;
--color-ths31-amber: oklch(0.769 0.165 70.1);
--color-ths31-red: #ef4444;
--color-ths31-red: oklch(0.637 0.208 25.3);
--color-ths31-violet: #8b5cf6;
--color-ths31-violet: oklch(0.606 0.219 292.7);
--color-ths31-cyan: #06b6d4;
--color-ths31-cyan: oklch(0.715 0.126 215.2);
--font-ths31-display: 'Bricolage Grotesque', system-ui, sans-serif;
--font-ths31-sans: 'Geist', system-ui, sans-serif;
--animate-ths31-mesh: ths31-mesh 16s ease-in-out infinite;
@keyframes ths31-mesh {
50% {
background-position: 100% 100%;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.ths-31 {
width: 100%;
min-height: 100svh;
}(() => {
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
document.querySelectorAll('[data-ths31-hero]').forEach((hero) => {
if (hero.dataset.wired || reduce) return;
hero.dataset.wired = '1';
const coarse = matchMedia('(pointer: coarse)').matches;
let tx = 0, ty = 0, x = 0, y = 0, raf = 0, orbit = 0, t0 = 0;
const paint = () => { hero.style.setProperty('--mx', x + 'px'); hero.style.setProperty('--my', y + 'px'); };
const centre = () => { const b = hero.getBoundingClientRect(); tx = b.width / 2; ty = b.height / 2; };
const loop = () => {
x += (tx - x) * 0.14; y += (ty - y) * 0.14; paint();
raf = (Math.abs(tx - x) > 0.5 || Math.abs(ty - y) > 0.5) ? requestAnimationFrame(loop) : 0;
};
const kick = () => { if (!raf) raf = requestAnimationFrame(loop); };
const startOrbit = () => {
if (orbit) return;
const step = (t) => {
if (!t0) t0 = t;
const s = (t - t0) / 1000, b = hero.getBoundingClientRect();
x = tx = b.width / 2 + Math.cos(s * 0.7) * b.width * 0.28;
y = ty = b.height / 2 + Math.sin(s * 1.4) * b.height * 0.22;
paint(); orbit = requestAnimationFrame(step);
};
orbit = requestAnimationFrame(step);
};
const stopOrbit = () => { cancelAnimationFrame(orbit); orbit = 0; t0 = 0; };
hero.addEventListener('pointermove', (e) => {
if (e.pointerType === 'touch' && !e.isPrimary) return;
stopOrbit();
const b = hero.getBoundingClientRect();
tx = e.clientX - b.left; ty = e.clientY - b.top; kick();
});
hero.addEventListener('pointerleave', () => { centre(); kick(); if (coarse) setTimeout(startOrbit, 800); });
const init = () => {
const b = hero.getBoundingClientRect();
if (b.width < 32 || b.height < 32) return requestAnimationFrame(init);
centre(); x = tx; y = ty; paint();
if (coarse) startOrbit();
};
init();
});
})();(() => {
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
document.querySelectorAll('[data-ths31-hero]').forEach((hero) => {
if (hero.dataset.wired || reduce) return;
hero.dataset.wired = '1';
const coarse = matchMedia('(pointer: coarse)').matches;
let tx = 0, ty = 0, x = 0, y = 0, raf = 0, orbit = 0, t0 = 0;
const paint = () => { hero.style.setProperty('--mx', x + 'px'); hero.style.setProperty('--my', y + 'px'); };
const centre = () => { const b = hero.getBoundingClientRect(); tx = b.width / 2; ty = b.height / 2; };
const loop = () => {
x += (tx - x) * 0.14; y += (ty - y) * 0.14; paint();
raf = (Math.abs(tx - x) > 0.5 || Math.abs(ty - y) > 0.5) ? requestAnimationFrame(loop) : 0;
};
const kick = () => { if (!raf) raf = requestAnimationFrame(loop); };
const startOrbit = () => {
if (orbit) return;
const step = (t) => {
if (!t0) t0 = t;
const s = (t - t0) / 1000, b = hero.getBoundingClientRect();
x = tx = b.width / 2 + Math.cos(s * 0.7) * b.width * 0.28;
y = ty = b.height / 2 + Math.sin(s * 1.4) * b.height * 0.22;
paint(); orbit = requestAnimationFrame(step);
};
orbit = requestAnimationFrame(step);
};
const stopOrbit = () => { cancelAnimationFrame(orbit); orbit = 0; t0 = 0; };
hero.addEventListener('pointermove', (e) => {
if (e.pointerType === 'touch' && !e.isPrimary) return;
stopOrbit();
const b = hero.getBoundingClientRect();
tx = e.clientX - b.left; ty = e.clientY - b.top; kick();
});
hero.addEventListener('pointerleave', () => { centre(); kick(); if (coarse) setTimeout(startOrbit, 800); });
const init = () => {
const b = hero.getBoundingClientRect();
if (b.width < 32 || b.height < 32) return requestAnimationFrame(init);
centre(); x = tx; y = ty; paint();
if (coarse) startOrbit();
};
init();
});
})();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-ths31-ink: #1c1030;
--color-ths31-ink: oklch(0.209 0.062 298.2);
--color-ths31-amber: #f59e0b;
--color-ths31-amber: oklch(0.769 0.165 70.1);
--color-ths31-red: #ef4444;
--color-ths31-red: oklch(0.637 0.208 25.3);
--color-ths31-violet: #8b5cf6;
--color-ths31-violet: oklch(0.606 0.219 292.7);
--color-ths31-cyan: #06b6d4;
--color-ths31-cyan: oklch(0.715 0.126 215.2);
--font-ths31-display: 'Bricolage Grotesque', system-ui, sans-serif;
--font-ths31-sans: 'Geist', system-ui, sans-serif;
--animate-ths31-mesh: ths31-mesh 16s ease-in-out infinite;
@keyframes ths31-mesh {
50% {
background-position: 100% 100%;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.ths-31 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-ths31-ink: #1c1030;
--color-ths31-ink: oklch(0.209 0.062 298.2);
--color-ths31-amber: #f59e0b;
--color-ths31-amber: oklch(0.769 0.165 70.1);
--color-ths31-red: #ef4444;
--color-ths31-red: oklch(0.637 0.208 25.3);
--color-ths31-violet: #8b5cf6;
--color-ths31-violet: oklch(0.606 0.219 292.7);
--color-ths31-cyan: #06b6d4;
--color-ths31-cyan: oklch(0.715 0.126 215.2);
--font-ths31-display: 'Bricolage Grotesque', system-ui, sans-serif;
--font-ths31-sans: 'Geist', system-ui, sans-serif;
--animate-ths31-mesh: ths31-mesh 16s ease-in-out infinite;
@keyframes ths31-mesh {
50% {
background-position: 100% 100%;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.ths-31 {
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=Bricolage+Grotesque:opsz,wght@12..96,500..800&family=Geist:wght@400;500;600&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=Bricolage+Grotesque:opsz,wght@12..96,500..800&family=Geist:wght@400;500;600&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The lens's
transform: translate(calc(var(--mx) - var(--r)), …)becameleft-(--mx) top-(--my) -translate-1/2— no radius variable, and the size is a singlesize-48. #hs-31/#hs-31-lensids becamedata-hooks with a wired guard, so the script is safe to run twice and supports multiple heroes.- UI upgrade from the CSS original: the hero had no call to action; it now has primary and glass secondary CTAs the lens glides over (it is
pointer-events-none). - UI upgrade: the script exits under reduced motion, a soft scrim lifts headline contrast on the bright mesh, and Bricolage Grotesque replaces Segoe UI.
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: Mouse-Tracking Liquid Glass Lens Hero in Tailwind CSS
Source: https://codefronts.com/layouts/tailwind-css-hero-sections/tailwind-mouse-tracking-liquid-glass-lens-hero/
A Liquid Glass lens that follows the pointer across a shifting conic-mesh hero, refracting the headline and colours beneath it with an eased spring; on touch it drifts in a slow figure-eight until you drag. Now with real CTAs and a reduced-motion path.
## HTML
```html
<section data-ths31-hero style="--mx:50%;--my:50%" class="ths-31 relative isolate grid w-full min-h-svh cursor-crosshair touch-pan-y content-center overflow-hidden bg-[conic-gradient(from_60deg_at_30%_30%,var(--color-ths31-amber),var(--color-ths31-red),var(--color-ths31-violet),var(--color-ths31-cyan),var(--color-ths31-amber))] bg-size-[200%_200%] px-6 py-16 font-ths31-sans text-white animate-ths31-mesh motion-reduce:animate-none sm:px-12" aria-label="Mouse-tracking liquid glass hero">
<div aria-hidden="true" class="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(70%_60%_at_20%_60%,rgb(0_0_0/0.28),transparent_70%)]"></div>
<div class="max-w-3xl">
<p class="text-[13px] font-semibold tracking-[0.14em] uppercase [text-shadow:0_1px_8px_rgb(0_0_0/0.3)]">Frontend that feels physical</p>
<h1 class="mt-3 font-ths31-display text-[clamp(64px,14vw,150px)] leading-[0.88] font-extrabold tracking-[-0.045em] [text-shadow:0_4px_24px_rgb(0_0_0/0.28)]">Liquid<br>Glass</h1>
<p class="mt-5 max-w-[34ch] text-base/relaxed [text-shadow:0_1px_8px_rgb(0_0_0/0.35)]">Move — or tap and drag on touch — the lens refracts everything beneath it.</p>
<div class="mt-8 flex flex-wrap gap-3">
<a href="#work" class="inline-flex min-h-12 cursor-pointer items-center rounded-full bg-white px-7 text-[15px] font-semibold text-ths31-ink shadow-[0_12px_30px_-10px_rgb(0_0_0/0.45)] transition hover:-translate-y-px focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white motion-reduce:transition-none">See the work</a>
<a href="#source" class="inline-flex min-h-12 cursor-pointer items-center rounded-full border border-white/60 bg-white/10 px-6 text-[15px] font-semibold text-white backdrop-blur-sm transition hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white">View source</a>
</div>
</div>
<span aria-hidden="true" data-ths31-lens class="pointer-events-none absolute top-(--my) left-(--mx) size-48 -translate-1/2 rounded-full border-2 border-white/70 bg-[radial-gradient(circle_at_35%_30%,rgb(255_255_255/0.32),rgb(255_255_255/0.05)_55%,transparent_72%)] shadow-[inset_0_3px_12px_rgb(255_255_255/0.55),inset_0_-12px_26px_rgb(0_0_0/0.28),0_14px_40px_-8px_rgb(0_0_0/0.55)] backdrop-blur-md backdrop-contrast-110 backdrop-saturate-200 motion-reduce:hidden"></span>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ths31-ink: #1c1030;
--color-ths31-ink: oklch(0.209 0.062 298.2);
--color-ths31-amber: #f59e0b;
--color-ths31-amber: oklch(0.769 0.165 70.1);
--color-ths31-red: #ef4444;
--color-ths31-red: oklch(0.637 0.208 25.3);
--color-ths31-violet: #8b5cf6;
--color-ths31-violet: oklch(0.606 0.219 292.7);
--color-ths31-cyan: #06b6d4;
--color-ths31-cyan: oklch(0.715 0.126 215.2);
--font-ths31-display: 'Bricolage Grotesque', system-ui, sans-serif;
--font-ths31-sans: 'Geist', system-ui, sans-serif;
--animate-ths31-mesh: ths31-mesh 16s ease-in-out infinite;
@keyframes ths31-mesh {
50% {
background-position: 100% 100%;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.ths-31 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
document.querySelectorAll('[data-ths31-hero]').forEach((hero) => {
if (hero.dataset.wired || reduce) return;
hero.dataset.wired = '1';
const coarse = matchMedia('(pointer: coarse)').matches;
let tx = 0, ty = 0, x = 0, y = 0, raf = 0, orbit = 0, t0 = 0;
const paint = () => { hero.style.setProperty('--mx', x + 'px'); hero.style.setProperty('--my', y + 'px'); };
const centre = () => { const b = hero.getBoundingClientRect(); tx = b.width / 2; ty = b.height / 2; };
const loop = () => {
x += (tx - x) * 0.14; y += (ty - y) * 0.14; paint();
raf = (Math.abs(tx - x) > 0.5 || Math.abs(ty - y) > 0.5) ? requestAnimationFrame(loop) : 0;
};
const kick = () => { if (!raf) raf = requestAnimationFrame(loop); };
const startOrbit = () => {
if (orbit) return;
const step = (t) => {
if (!t0) t0 = t;
const s = (t - t0) / 1000, b = hero.getBoundingClientRect();
x = tx = b.width / 2 + Math.cos(s * 0.7) * b.width * 0.28;
y = ty = b.height / 2 + Math.sin(s * 1.4) * b.height * 0.22;
paint(); orbit = requestAnimationFrame(step);
};
orbit = requestAnimationFrame(step);
};
const stopOrbit = () => { cancelAnimationFrame(orbit); orbit = 0; t0 = 0; };
hero.addEventListener('pointermove', (e) => {
if (e.pointerType === 'touch' && !e.isPrimary) return;
stopOrbit();
const b = hero.getBoundingClientRect();
tx = e.clientX - b.left; ty = e.clientY - b.top; kick();
});
hero.addEventListener('pointerleave', () => { centre(); kick(); if (coarse) setTimeout(startOrbit, 800); });
const init = () => {
const b = hero.getBoundingClientRect();
if (b.width < 32 || b.height < 32) return requestAnimationFrame(init);
centre(); x = tx; y = ty; paint();
if (coarse) startOrbit();
};
init();
});
})();
```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: Mouse-Tracking Liquid Glass Lens Hero in Tailwind CSS
Source: https://codefronts.com/layouts/tailwind-css-hero-sections/tailwind-mouse-tracking-liquid-glass-lens-hero/
A Liquid Glass lens that follows the pointer across a shifting conic-mesh hero, refracting the headline and colours beneath it with an eased spring; on touch it drifts in a slow figure-eight until you drag. Now with real CTAs and a reduced-motion path.
## HTML
```html
<section data-ths31-hero style="--mx:50%;--my:50%" class="ths-31 relative isolate grid w-full min-h-svh cursor-crosshair touch-pan-y content-center overflow-hidden bg-[conic-gradient(from_60deg_at_30%_30%,var(--color-ths31-amber),var(--color-ths31-red),var(--color-ths31-violet),var(--color-ths31-cyan),var(--color-ths31-amber))] bg-size-[200%_200%] px-6 py-16 font-ths31-sans text-white animate-ths31-mesh motion-reduce:animate-none sm:px-12" aria-label="Mouse-tracking liquid glass hero">
<div aria-hidden="true" class="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(70%_60%_at_20%_60%,rgb(0_0_0/0.28),transparent_70%)]"></div>
<div class="max-w-3xl">
<p class="text-[13px] font-semibold tracking-[0.14em] uppercase [text-shadow:0_1px_8px_rgb(0_0_0/0.3)]">Frontend that feels physical</p>
<h1 class="mt-3 font-ths31-display text-[clamp(64px,14vw,150px)] leading-[0.88] font-extrabold tracking-[-0.045em] [text-shadow:0_4px_24px_rgb(0_0_0/0.28)]">Liquid<br>Glass</h1>
<p class="mt-5 max-w-[34ch] text-base/relaxed [text-shadow:0_1px_8px_rgb(0_0_0/0.35)]">Move — or tap and drag on touch — the lens refracts everything beneath it.</p>
<div class="mt-8 flex flex-wrap gap-3">
<a href="#work" class="inline-flex min-h-12 cursor-pointer items-center rounded-full bg-white px-7 text-[15px] font-semibold text-ths31-ink shadow-[0_12px_30px_-10px_rgb(0_0_0/0.45)] transition hover:-translate-y-px focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white motion-reduce:transition-none">See the work</a>
<a href="#source" class="inline-flex min-h-12 cursor-pointer items-center rounded-full border border-white/60 bg-white/10 px-6 text-[15px] font-semibold text-white backdrop-blur-sm transition hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white">View source</a>
</div>
</div>
<span aria-hidden="true" data-ths31-lens class="pointer-events-none absolute top-(--my) left-(--mx) size-48 -translate-1/2 rounded-full border-2 border-white/70 bg-[radial-gradient(circle_at_35%_30%,rgb(255_255_255/0.32),rgb(255_255_255/0.05)_55%,transparent_72%)] shadow-[inset_0_3px_12px_rgb(255_255_255/0.55),inset_0_-12px_26px_rgb(0_0_0/0.28),0_14px_40px_-8px_rgb(0_0_0/0.55)] backdrop-blur-md backdrop-contrast-110 backdrop-saturate-200 motion-reduce:hidden"></span>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-ths31-ink: #1c1030;
--color-ths31-ink: oklch(0.209 0.062 298.2);
--color-ths31-amber: #f59e0b;
--color-ths31-amber: oklch(0.769 0.165 70.1);
--color-ths31-red: #ef4444;
--color-ths31-red: oklch(0.637 0.208 25.3);
--color-ths31-violet: #8b5cf6;
--color-ths31-violet: oklch(0.606 0.219 292.7);
--color-ths31-cyan: #06b6d4;
--color-ths31-cyan: oklch(0.715 0.126 215.2);
--font-ths31-display: 'Bricolage Grotesque', system-ui, sans-serif;
--font-ths31-sans: 'Geist', system-ui, sans-serif;
--animate-ths31-mesh: ths31-mesh 16s ease-in-out infinite;
@keyframes ths31-mesh {
50% {
background-position: 100% 100%;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.ths-31 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
document.querySelectorAll('[data-ths31-hero]').forEach((hero) => {
if (hero.dataset.wired || reduce) return;
hero.dataset.wired = '1';
const coarse = matchMedia('(pointer: coarse)').matches;
let tx = 0, ty = 0, x = 0, y = 0, raf = 0, orbit = 0, t0 = 0;
const paint = () => { hero.style.setProperty('--mx', x + 'px'); hero.style.setProperty('--my', y + 'px'); };
const centre = () => { const b = hero.getBoundingClientRect(); tx = b.width / 2; ty = b.height / 2; };
const loop = () => {
x += (tx - x) * 0.14; y += (ty - y) * 0.14; paint();
raf = (Math.abs(tx - x) > 0.5 || Math.abs(ty - y) > 0.5) ? requestAnimationFrame(loop) : 0;
};
const kick = () => { if (!raf) raf = requestAnimationFrame(loop); };
const startOrbit = () => {
if (orbit) return;
const step = (t) => {
if (!t0) t0 = t;
const s = (t - t0) / 1000, b = hero.getBoundingClientRect();
x = tx = b.width / 2 + Math.cos(s * 0.7) * b.width * 0.28;
y = ty = b.height / 2 + Math.sin(s * 1.4) * b.height * 0.22;
paint(); orbit = requestAnimationFrame(step);
};
orbit = requestAnimationFrame(step);
};
const stopOrbit = () => { cancelAnimationFrame(orbit); orbit = 0; t0 = 0; };
hero.addEventListener('pointermove', (e) => {
if (e.pointerType === 'touch' && !e.isPrimary) return;
stopOrbit();
const b = hero.getBoundingClientRect();
tx = e.clientX - b.left; ty = e.clientY - b.top; kick();
});
hero.addEventListener('pointerleave', () => { centre(); kick(); if (coarse) setTimeout(startOrbit, 800); });
const init = () => {
const b = hero.getBoundingClientRect();
if (b.width < 32 || b.height < 32) return requestAnimationFrame(init);
centre(); x = tx; y = ty; paint();
if (coarse) startOrbit();
};
init();
});
})();
```How this works
JS writes two numbers. The controller sets --mx and --my on the hero; the lens positions itself with left-(--mx) top-(--my) and -translate-1/2. All visual decisions stay in the class list.
Polite motion. The spring loop stops when the lens settles; on coarse pointers an ambient orbit shows the effect without input; with prefers-reduced-motion the script exits and motion-reduce:hidden removes the lens entirely.
Make it yours
- Lens size: change
size-48. - Stronger refraction:
backdrop-blur-lg backdrop-saturate-[2.4]. - Spring stiffness: the
0.14factor in the script (higher = snappier).
Gotchas — read before shipping
- The lens is
pointer-events-noneso it never blocks the CTAs beneath it. - Data hooks (
data-ths31-hero) instead of ids let several heroes coexist.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
backdrop-filter and pointer events are universal on Tailwind v4's floor.