25 / 30
All 30 in 30 Tailwind CSS Text Hover Effects
- 01Tailwind CSS Animated Underline Hover Effect
- 02Tailwind Liquid Text Fill Hover Animation
- 03Tailwind Gradient Text Hover Effect with bg-clip-text
- 04Tailwind Animated Strikethrough Text Hover Effect
- 05Tailwind Text Swap and Flip Up Hover Effect
- 06Tailwind CSS Glitch Text Hover Effect
- 07Tailwind Neon Glow Text with Flicker Animation
- 08Tailwind Metallic Gold Shimmer Text Effect
- 09Tailwind 3D Text Pop and Lift Hover Effect
- 10Tailwind Staggered Letter Wave Hover Animation
- 11Tailwind Spotlight Text Reveal on Mouse Hover
- 12Tailwind Hover to Reveal Hidden Spoiler Text
- 13Tailwind Blur to Focus Text Hover Effect
- 14Tailwind Magnetic Hover Effect for Buttons and Text
- 15Tailwind Letter Spacing Expand on Hover (Tracking Animation)
- 16Tailwind Outline Text Stroke to Fill Hover Effect
- 17Tailwind Text Scramble and Matrix Decrypt Hover Effect
- 18Tailwind Animated Border Button Hover Effect
- 19Tailwind Elastic Rubber Band Text Hover Animation
- 20Tailwind Variable Font Weight Hover Animation
- 21Tailwind Animated Rainbow Gradient Text on Hover
- 22Tailwind 3D Perspective Tilt Text Hover Effect
- 23Tailwind Smoke Dissolve Text Hover Effect
- 24Tailwind Fire Text Effect with Heat Wave Glow
- 25Tailwind Dynamic Text Shadow that Follows the Cursor
- 26Tailwind Infinite Text Marquee that Speeds Up on Hover
- 27Tailwind Duotone Split Color Text Hover Effect
- 28Tailwind CSS Tooltip on Hover (No JavaScript)
- 29Tailwind SVG Water Ripple Distortion Text Effect
- 30Tailwind Clip-Path Text Reveal Hover Animation
Tailwind Dynamic Text Shadow that Follows the Cursor
You hold the lamp: every heading and card casts a shadow away from the cursor, longer and softer with distance, while a pool of light follows the pointer across the stage. The script writes --tth25-sx/sy/sb; the markup consumes them in [text-shadow:var(--tth25-sx,0px)_var(--tth25-sy,5px)_…] — and the fallbacks are the resting design.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tth-25 @container w-full min-h-svh grid place-items-center bg-tth25-bg bg-[radial-gradient(340px_340px_at_var(--tth25-lx,50%)_var(--tth25-ly,25%),oklch(0.99_0.03_90/0.9),transparent_70%)] px-5 py-14 font-tth25-sans text-tth25-ink" data-tth25-board aria-label="Dynamic light source shadow demo">
<div class="flex w-full max-w-[53.75rem] flex-col gap-[clamp(20px,3.6cqi,32px)]">
<header class="flex flex-col items-center gap-3 text-center">
<h2 data-tth25-lit class="text-[clamp(40px,8cqi,76px)] font-extrabold leading-none tracking-[-0.03em] [text-shadow:var(--tth25-sx,0px)_var(--tth25-sy,5px)_var(--tth25-sb,9px)_color-mix(in_oklch,var(--color-tth25-sh)_38%,transparent)] transition-[text-shadow] duration-80 ease-linear motion-reduce:transition-none">The Lamp Test</h2>
<p class="max-w-[52ch] text-[15px] leading-[1.65] text-tth25-mut">Move the cursor — you're holding the light. Every shadow points away from it; length and blur grow with distance.</p>
</header>
<div class="grid grid-cols-3 gap-[clamp(12px,2.4cqi,20px)] @max-[38rem]:grid-cols-1">
<a href="#tth25-top" data-tth25-lit class="flex flex-col gap-2 rounded-2xl border border-tth25-line bg-tth25-card p-[clamp(16px,3cqi,24px)] no-underline shadow-[var(--tth25-sx,0px)_var(--tth25-sy,6px)_calc(var(--tth25-sb,9px)*2.4)_-6px_color-mix(in_oklch,var(--color-tth25-sh)_22%,transparent)] transition-[border-color,box-shadow] duration-80 hover:border-tth25-ink focus-visible:border-tth25-ink focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tth25-ink"><h3 data-tth25-lit class="text-[clamp(22px,3.6cqi,30px)] font-extrabold tracking-[-0.02em] [text-shadow:var(--tth25-sx,0px)_var(--tth25-sy,5px)_var(--tth25-sb,9px)_color-mix(in_oklch,var(--color-tth25-sh)_38%,transparent)] transition-[text-shadow] duration-80 ease-linear">Umbra</h3><p class="text-[13.5px] leading-[1.6] text-tth25-mut">The hard core of a shadow — short and sharp when light is near.</p></a>
<a href="#tth25-top" data-tth25-lit class="flex flex-col gap-2 rounded-2xl border border-tth25-line bg-tth25-card p-[clamp(16px,3cqi,24px)] no-underline shadow-[var(--tth25-sx,0px)_var(--tth25-sy,6px)_calc(var(--tth25-sb,9px)*2.4)_-6px_color-mix(in_oklch,var(--color-tth25-sh)_22%,transparent)] transition-[border-color,box-shadow] duration-80 hover:border-tth25-ink focus-visible:border-tth25-ink focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tth25-ink"><h3 data-tth25-lit class="text-[clamp(22px,3.6cqi,30px)] font-extrabold tracking-[-0.02em] [text-shadow:var(--tth25-sx,0px)_var(--tth25-sy,5px)_var(--tth25-sb,9px)_color-mix(in_oklch,var(--color-tth25-sh)_38%,transparent)] transition-[text-shadow] duration-80 ease-linear">Penumbra</h3><p class="text-[13.5px] leading-[1.6] text-tth25-mut">The soft fringe — watch blur climb as the lamp walks away.</p></a>
<a href="#tth25-top" data-tth25-lit class="flex flex-col gap-2 rounded-2xl border border-tth25-line bg-tth25-card p-[clamp(16px,3cqi,24px)] no-underline shadow-[var(--tth25-sx,0px)_var(--tth25-sy,6px)_calc(var(--tth25-sb,9px)*2.4)_-6px_color-mix(in_oklch,var(--color-tth25-sh)_22%,transparent)] transition-[border-color,box-shadow] duration-80 hover:border-tth25-ink focus-visible:border-tth25-ink focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tth25-ink"><h3 data-tth25-lit class="text-[clamp(22px,3.6cqi,30px)] font-extrabold tracking-[-0.02em] [text-shadow:var(--tth25-sx,0px)_var(--tth25-sy,5px)_var(--tth25-sb,9px)_color-mix(in_oklch,var(--color-tth25-sh)_38%,transparent)] transition-[text-shadow] duration-80 ease-linear">Cast</h3><p class="text-[13.5px] leading-[1.6] text-tth25-mut">Direction disagrees per card — that's what sells one shared lamp.</p></a>
</div>
<p id="tth25-top" class="text-center text-[12.5px] text-tth25-mut">JS writes <code class="rounded bg-tth25-chip px-1.5 py-px font-tth25-mono text-[0.9em]">--tth25-sx/sy/sb</code> per element; Tailwind owns the shadows. No pointer? The fallback values <em>are</em> the resting design.</p>
</div>
</section><section class="tth-25 @container w-full min-h-svh grid place-items-center bg-tth25-bg bg-[radial-gradient(340px_340px_at_var(--tth25-lx,50%)_var(--tth25-ly,25%),oklch(0.99_0.03_90/0.9),transparent_70%)] px-5 py-14 font-tth25-sans text-tth25-ink" data-tth25-board aria-label="Dynamic light source shadow demo">
<div class="flex w-full max-w-[53.75rem] flex-col gap-[clamp(20px,3.6cqi,32px)]">
<header class="flex flex-col items-center gap-3 text-center">
<h2 data-tth25-lit class="text-[clamp(40px,8cqi,76px)] font-extrabold leading-none tracking-[-0.03em] [text-shadow:var(--tth25-sx,0px)_var(--tth25-sy,5px)_var(--tth25-sb,9px)_color-mix(in_oklch,var(--color-tth25-sh)_38%,transparent)] transition-[text-shadow] duration-80 ease-linear motion-reduce:transition-none">The Lamp Test</h2>
<p class="max-w-[52ch] text-[15px] leading-[1.65] text-tth25-mut">Move the cursor — you're holding the light. Every shadow points away from it; length and blur grow with distance.</p>
</header>
<div class="grid grid-cols-3 gap-[clamp(12px,2.4cqi,20px)] @max-[38rem]:grid-cols-1">
<a href="#tth25-top" data-tth25-lit class="flex flex-col gap-2 rounded-2xl border border-tth25-line bg-tth25-card p-[clamp(16px,3cqi,24px)] no-underline shadow-[var(--tth25-sx,0px)_var(--tth25-sy,6px)_calc(var(--tth25-sb,9px)*2.4)_-6px_color-mix(in_oklch,var(--color-tth25-sh)_22%,transparent)] transition-[border-color,box-shadow] duration-80 hover:border-tth25-ink focus-visible:border-tth25-ink focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tth25-ink"><h3 data-tth25-lit class="text-[clamp(22px,3.6cqi,30px)] font-extrabold tracking-[-0.02em] [text-shadow:var(--tth25-sx,0px)_var(--tth25-sy,5px)_var(--tth25-sb,9px)_color-mix(in_oklch,var(--color-tth25-sh)_38%,transparent)] transition-[text-shadow] duration-80 ease-linear">Umbra</h3><p class="text-[13.5px] leading-[1.6] text-tth25-mut">The hard core of a shadow — short and sharp when light is near.</p></a>
<a href="#tth25-top" data-tth25-lit class="flex flex-col gap-2 rounded-2xl border border-tth25-line bg-tth25-card p-[clamp(16px,3cqi,24px)] no-underline shadow-[var(--tth25-sx,0px)_var(--tth25-sy,6px)_calc(var(--tth25-sb,9px)*2.4)_-6px_color-mix(in_oklch,var(--color-tth25-sh)_22%,transparent)] transition-[border-color,box-shadow] duration-80 hover:border-tth25-ink focus-visible:border-tth25-ink focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tth25-ink"><h3 data-tth25-lit class="text-[clamp(22px,3.6cqi,30px)] font-extrabold tracking-[-0.02em] [text-shadow:var(--tth25-sx,0px)_var(--tth25-sy,5px)_var(--tth25-sb,9px)_color-mix(in_oklch,var(--color-tth25-sh)_38%,transparent)] transition-[text-shadow] duration-80 ease-linear">Penumbra</h3><p class="text-[13.5px] leading-[1.6] text-tth25-mut">The soft fringe — watch blur climb as the lamp walks away.</p></a>
<a href="#tth25-top" data-tth25-lit class="flex flex-col gap-2 rounded-2xl border border-tth25-line bg-tth25-card p-[clamp(16px,3cqi,24px)] no-underline shadow-[var(--tth25-sx,0px)_var(--tth25-sy,6px)_calc(var(--tth25-sb,9px)*2.4)_-6px_color-mix(in_oklch,var(--color-tth25-sh)_22%,transparent)] transition-[border-color,box-shadow] duration-80 hover:border-tth25-ink focus-visible:border-tth25-ink focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tth25-ink"><h3 data-tth25-lit class="text-[clamp(22px,3.6cqi,30px)] font-extrabold tracking-[-0.02em] [text-shadow:var(--tth25-sx,0px)_var(--tth25-sy,5px)_var(--tth25-sb,9px)_color-mix(in_oklch,var(--color-tth25-sh)_38%,transparent)] transition-[text-shadow] duration-80 ease-linear">Cast</h3><p class="text-[13.5px] leading-[1.6] text-tth25-mut">Direction disagrees per card — that's what sells one shared lamp.</p></a>
</div>
<p id="tth25-top" class="text-center text-[12.5px] text-tth25-mut">JS writes <code class="rounded bg-tth25-chip px-1.5 py-px font-tth25-mono text-[0.9em]">--tth25-sx/sy/sb</code> per element; Tailwind owns the shadows. No pointer? The fallback values <em>are</em> the resting design.</p>
</div>
</section>@import "tailwindcss";
@theme {
--color-tth25-bg: #f4f1ea;
--color-tth25-bg: oklch(0.95 0.008 85);
--color-tth25-card: #fcfbf8;
--color-tth25-card: oklch(0.985 0.005 85);
--color-tth25-chip: #e6e2d8;
--color-tth25-chip: oklch(0.9 0.01 85);
--color-tth25-ink: #1f1e2e;
--color-tth25-ink: oklch(0.24 0.02 275);
--color-tth25-mut: #5b5a6a;
--color-tth25-mut: oklch(0.47 0.02 275);
--color-tth25-line: #ddd8cc;
--color-tth25-line: oklch(0.88 0.012 85);
--color-tth25-sh: #252a3a;
--color-tth25-sh: oklch(0.25 0.03 265);
--font-tth25-sans: 'Bricolage Grotesque', system-ui, sans-serif;
--font-tth25-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tth-25 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tth25-bg: #f4f1ea;
--color-tth25-bg: oklch(0.95 0.008 85);
--color-tth25-card: #fcfbf8;
--color-tth25-card: oklch(0.985 0.005 85);
--color-tth25-chip: #e6e2d8;
--color-tth25-chip: oklch(0.9 0.01 85);
--color-tth25-ink: #1f1e2e;
--color-tth25-ink: oklch(0.24 0.02 275);
--color-tth25-mut: #5b5a6a;
--color-tth25-mut: oklch(0.47 0.02 275);
--color-tth25-line: #ddd8cc;
--color-tth25-line: oklch(0.88 0.012 85);
--color-tth25-sh: #252a3a;
--color-tth25-sh: oklch(0.25 0.03 265);
--font-tth25-sans: 'Bricolage Grotesque', system-ui, sans-serif;
--font-tth25-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tth-25 {
width: 100%;
min-height: 100svh;
}(function () {
var board = document.querySelector('[data-tth25-board]');
if (!board) return;
var targets = [].slice.call(board.querySelectorAll('[data-tth25-lit]'));
var rects = [];
function measure() { rects = targets.map(function (el) { return el.getBoundingClientRect(); }); }
measure();
addEventListener('resize', measure);
addEventListener('scroll', measure, true);
board.addEventListener('pointermove', function (e) {
var br = board.getBoundingClientRect();
board.style.setProperty('--tth25-lx', ((e.clientX - br.left) / br.width * 100).toFixed(1) + '%');
board.style.setProperty('--tth25-ly', ((e.clientY - br.top) / br.height * 100).toFixed(1) + '%');
targets.forEach(function (el, i) {
var r = rects[i] || el.getBoundingClientRect();
var dx = (r.left + r.width / 2) - e.clientX, dy = (r.top + r.height / 2) - e.clientY;
var d = Math.hypot(dx, dy) || 1, pull = Math.min(d / 14, 18);
el.style.setProperty('--tth25-sx', (dx / d * pull).toFixed(1) + 'px');
el.style.setProperty('--tth25-sy', (dy / d * pull).toFixed(1) + 'px');
el.style.setProperty('--tth25-sb', (2 + pull * 1.1).toFixed(1) + 'px');
});
});
})();(function () {
var board = document.querySelector('[data-tth25-board]');
if (!board) return;
var targets = [].slice.call(board.querySelectorAll('[data-tth25-lit]'));
var rects = [];
function measure() { rects = targets.map(function (el) { return el.getBoundingClientRect(); }); }
measure();
addEventListener('resize', measure);
addEventListener('scroll', measure, true);
board.addEventListener('pointermove', function (e) {
var br = board.getBoundingClientRect();
board.style.setProperty('--tth25-lx', ((e.clientX - br.left) / br.width * 100).toFixed(1) + '%');
board.style.setProperty('--tth25-ly', ((e.clientY - br.top) / br.height * 100).toFixed(1) + '%');
targets.forEach(function (el, i) {
var r = rects[i] || el.getBoundingClientRect();
var dx = (r.left + r.width / 2) - e.clientX, dy = (r.top + r.height / 2) - e.clientY;
var d = Math.hypot(dx, dy) || 1, pull = Math.min(d / 14, 18);
el.style.setProperty('--tth25-sx', (dx / d * pull).toFixed(1) + 'px');
el.style.setProperty('--tth25-sy', (dy / d * pull).toFixed(1) + 'px');
el.style.setProperty('--tth25-sb', (2 + pull * 1.1).toFixed(1) + 'px');
});
});
})();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-tth25-bg: #f4f1ea;
--color-tth25-bg: oklch(0.95 0.008 85);
--color-tth25-card: #fcfbf8;
--color-tth25-card: oklch(0.985 0.005 85);
--color-tth25-chip: #e6e2d8;
--color-tth25-chip: oklch(0.9 0.01 85);
--color-tth25-ink: #1f1e2e;
--color-tth25-ink: oklch(0.24 0.02 275);
--color-tth25-mut: #5b5a6a;
--color-tth25-mut: oklch(0.47 0.02 275);
--color-tth25-line: #ddd8cc;
--color-tth25-line: oklch(0.88 0.012 85);
--color-tth25-sh: #252a3a;
--color-tth25-sh: oklch(0.25 0.03 265);
--font-tth25-sans: 'Bricolage Grotesque', system-ui, sans-serif;
--font-tth25-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tth-25 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tth25-bg: #f4f1ea;
--color-tth25-bg: oklch(0.95 0.008 85);
--color-tth25-card: #fcfbf8;
--color-tth25-card: oklch(0.985 0.005 85);
--color-tth25-chip: #e6e2d8;
--color-tth25-chip: oklch(0.9 0.01 85);
--color-tth25-ink: #1f1e2e;
--color-tth25-ink: oklch(0.24 0.02 275);
--color-tth25-mut: #5b5a6a;
--color-tth25-mut: oklch(0.47 0.02 275);
--color-tth25-line: #ddd8cc;
--color-tth25-line: oklch(0.88 0.012 85);
--color-tth25-sh: #252a3a;
--color-tth25-sh: oklch(0.25 0.03 265);
--font-tth25-sans: 'Bricolage Grotesque', system-ui, sans-serif;
--font-tth25-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tth-25 {
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,400..800&family=JetBrains+Mono:wght@400;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,400..800&family=JetBrains+Mono:wght@400;600&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The script now selects
[data-tth25-board]/[data-tth25-lit]and writes namespaced--tth25-*properties;.lit{text-shadow:var(--sx,0px)…}became an arbitrary[text-shadow:…]utility. @container cth25 (width < 620px)became@container+@max-[38rem]:grid-cols-1.- UI upgrade from the CSS original: the cards themselves now cast box-shadows from the same lamp — only the headings did before, which made the cards look pasted flat while their titles floated.
- UI upgrade: rect caching listens for scroll in capture mode (any scroll container, not just the board), fixing drifting shadows after page scroll; Bricolage Grotesque replaced Segoe.
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 Dynamic Text Shadow that Follows the Cursor
Source: https://codefronts.com/motion/tailwind-css-text-hover-effects/tailwind-dynamic-text-shadow-that-follows-the-cursor/
You hold the lamp: every heading and card casts a shadow away from the cursor, longer and softer with distance, while a pool of light follows the pointer across the stage. The script writes --tth25-sx/sy/sb; the markup consumes them in [text-shadow:var(--tth25-sx,0px)_var(--tth25-sy,5px)_…] — and the fallbacks are the resting design.
## HTML
```html
<section class="tth-25 @container w-full min-h-svh grid place-items-center bg-tth25-bg bg-[radial-gradient(340px_340px_at_var(--tth25-lx,50%)_var(--tth25-ly,25%),oklch(0.99_0.03_90/0.9),transparent_70%)] px-5 py-14 font-tth25-sans text-tth25-ink" data-tth25-board aria-label="Dynamic light source shadow demo">
<div class="flex w-full max-w-[53.75rem] flex-col gap-[clamp(20px,3.6cqi,32px)]">
<header class="flex flex-col items-center gap-3 text-center">
<h2 data-tth25-lit class="text-[clamp(40px,8cqi,76px)] font-extrabold leading-none tracking-[-0.03em] [text-shadow:var(--tth25-sx,0px)_var(--tth25-sy,5px)_var(--tth25-sb,9px)_color-mix(in_oklch,var(--color-tth25-sh)_38%,transparent)] transition-[text-shadow] duration-80 ease-linear motion-reduce:transition-none">The Lamp Test</h2>
<p class="max-w-[52ch] text-[15px] leading-[1.65] text-tth25-mut">Move the cursor — you're holding the light. Every shadow points away from it; length and blur grow with distance.</p>
</header>
<div class="grid grid-cols-3 gap-[clamp(12px,2.4cqi,20px)] @max-[38rem]:grid-cols-1">
<a href="#tth25-top" data-tth25-lit class="flex flex-col gap-2 rounded-2xl border border-tth25-line bg-tth25-card p-[clamp(16px,3cqi,24px)] no-underline shadow-[var(--tth25-sx,0px)_var(--tth25-sy,6px)_calc(var(--tth25-sb,9px)*2.4)_-6px_color-mix(in_oklch,var(--color-tth25-sh)_22%,transparent)] transition-[border-color,box-shadow] duration-80 hover:border-tth25-ink focus-visible:border-tth25-ink focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tth25-ink"><h3 data-tth25-lit class="text-[clamp(22px,3.6cqi,30px)] font-extrabold tracking-[-0.02em] [text-shadow:var(--tth25-sx,0px)_var(--tth25-sy,5px)_var(--tth25-sb,9px)_color-mix(in_oklch,var(--color-tth25-sh)_38%,transparent)] transition-[text-shadow] duration-80 ease-linear">Umbra</h3><p class="text-[13.5px] leading-[1.6] text-tth25-mut">The hard core of a shadow — short and sharp when light is near.</p></a>
<a href="#tth25-top" data-tth25-lit class="flex flex-col gap-2 rounded-2xl border border-tth25-line bg-tth25-card p-[clamp(16px,3cqi,24px)] no-underline shadow-[var(--tth25-sx,0px)_var(--tth25-sy,6px)_calc(var(--tth25-sb,9px)*2.4)_-6px_color-mix(in_oklch,var(--color-tth25-sh)_22%,transparent)] transition-[border-color,box-shadow] duration-80 hover:border-tth25-ink focus-visible:border-tth25-ink focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tth25-ink"><h3 data-tth25-lit class="text-[clamp(22px,3.6cqi,30px)] font-extrabold tracking-[-0.02em] [text-shadow:var(--tth25-sx,0px)_var(--tth25-sy,5px)_var(--tth25-sb,9px)_color-mix(in_oklch,var(--color-tth25-sh)_38%,transparent)] transition-[text-shadow] duration-80 ease-linear">Penumbra</h3><p class="text-[13.5px] leading-[1.6] text-tth25-mut">The soft fringe — watch blur climb as the lamp walks away.</p></a>
<a href="#tth25-top" data-tth25-lit class="flex flex-col gap-2 rounded-2xl border border-tth25-line bg-tth25-card p-[clamp(16px,3cqi,24px)] no-underline shadow-[var(--tth25-sx,0px)_var(--tth25-sy,6px)_calc(var(--tth25-sb,9px)*2.4)_-6px_color-mix(in_oklch,var(--color-tth25-sh)_22%,transparent)] transition-[border-color,box-shadow] duration-80 hover:border-tth25-ink focus-visible:border-tth25-ink focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tth25-ink"><h3 data-tth25-lit class="text-[clamp(22px,3.6cqi,30px)] font-extrabold tracking-[-0.02em] [text-shadow:var(--tth25-sx,0px)_var(--tth25-sy,5px)_var(--tth25-sb,9px)_color-mix(in_oklch,var(--color-tth25-sh)_38%,transparent)] transition-[text-shadow] duration-80 ease-linear">Cast</h3><p class="text-[13.5px] leading-[1.6] text-tth25-mut">Direction disagrees per card — that's what sells one shared lamp.</p></a>
</div>
<p id="tth25-top" class="text-center text-[12.5px] text-tth25-mut">JS writes <code class="rounded bg-tth25-chip px-1.5 py-px font-tth25-mono text-[0.9em]">--tth25-sx/sy/sb</code> per element; Tailwind owns the shadows. No pointer? The fallback values <em>are</em> the resting design.</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tth25-bg: #f4f1ea;
--color-tth25-bg: oklch(0.95 0.008 85);
--color-tth25-card: #fcfbf8;
--color-tth25-card: oklch(0.985 0.005 85);
--color-tth25-chip: #e6e2d8;
--color-tth25-chip: oklch(0.9 0.01 85);
--color-tth25-ink: #1f1e2e;
--color-tth25-ink: oklch(0.24 0.02 275);
--color-tth25-mut: #5b5a6a;
--color-tth25-mut: oklch(0.47 0.02 275);
--color-tth25-line: #ddd8cc;
--color-tth25-line: oklch(0.88 0.012 85);
--color-tth25-sh: #252a3a;
--color-tth25-sh: oklch(0.25 0.03 265);
--font-tth25-sans: 'Bricolage Grotesque', system-ui, sans-serif;
--font-tth25-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tth-25 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(function () {
var board = document.querySelector('[data-tth25-board]');
if (!board) return;
var targets = [].slice.call(board.querySelectorAll('[data-tth25-lit]'));
var rects = [];
function measure() { rects = targets.map(function (el) { return el.getBoundingClientRect(); }); }
measure();
addEventListener('resize', measure);
addEventListener('scroll', measure, true);
board.addEventListener('pointermove', function (e) {
var br = board.getBoundingClientRect();
board.style.setProperty('--tth25-lx', ((e.clientX - br.left) / br.width * 100).toFixed(1) + '%');
board.style.setProperty('--tth25-ly', ((e.clientY - br.top) / br.height * 100).toFixed(1) + '%');
targets.forEach(function (el, i) {
var r = rects[i] || el.getBoundingClientRect();
var dx = (r.left + r.width / 2) - e.clientX, dy = (r.top + r.height / 2) - e.clientY;
var d = Math.hypot(dx, dy) || 1, pull = Math.min(d / 14, 18);
el.style.setProperty('--tth25-sx', (dx / d * pull).toFixed(1) + 'px');
el.style.setProperty('--tth25-sy', (dy / d * pull).toFixed(1) + 'px');
el.style.setProperty('--tth25-sb', (2 + pull * 1.1).toFixed(1) + 'px');
});
});
})();
```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 Dynamic Text Shadow that Follows the Cursor
Source: https://codefronts.com/motion/tailwind-css-text-hover-effects/tailwind-dynamic-text-shadow-that-follows-the-cursor/
You hold the lamp: every heading and card casts a shadow away from the cursor, longer and softer with distance, while a pool of light follows the pointer across the stage. The script writes --tth25-sx/sy/sb; the markup consumes them in [text-shadow:var(--tth25-sx,0px)_var(--tth25-sy,5px)_…] — and the fallbacks are the resting design.
## HTML
```html
<section class="tth-25 @container w-full min-h-svh grid place-items-center bg-tth25-bg bg-[radial-gradient(340px_340px_at_var(--tth25-lx,50%)_var(--tth25-ly,25%),oklch(0.99_0.03_90/0.9),transparent_70%)] px-5 py-14 font-tth25-sans text-tth25-ink" data-tth25-board aria-label="Dynamic light source shadow demo">
<div class="flex w-full max-w-[53.75rem] flex-col gap-[clamp(20px,3.6cqi,32px)]">
<header class="flex flex-col items-center gap-3 text-center">
<h2 data-tth25-lit class="text-[clamp(40px,8cqi,76px)] font-extrabold leading-none tracking-[-0.03em] [text-shadow:var(--tth25-sx,0px)_var(--tth25-sy,5px)_var(--tth25-sb,9px)_color-mix(in_oklch,var(--color-tth25-sh)_38%,transparent)] transition-[text-shadow] duration-80 ease-linear motion-reduce:transition-none">The Lamp Test</h2>
<p class="max-w-[52ch] text-[15px] leading-[1.65] text-tth25-mut">Move the cursor — you're holding the light. Every shadow points away from it; length and blur grow with distance.</p>
</header>
<div class="grid grid-cols-3 gap-[clamp(12px,2.4cqi,20px)] @max-[38rem]:grid-cols-1">
<a href="#tth25-top" data-tth25-lit class="flex flex-col gap-2 rounded-2xl border border-tth25-line bg-tth25-card p-[clamp(16px,3cqi,24px)] no-underline shadow-[var(--tth25-sx,0px)_var(--tth25-sy,6px)_calc(var(--tth25-sb,9px)*2.4)_-6px_color-mix(in_oklch,var(--color-tth25-sh)_22%,transparent)] transition-[border-color,box-shadow] duration-80 hover:border-tth25-ink focus-visible:border-tth25-ink focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tth25-ink"><h3 data-tth25-lit class="text-[clamp(22px,3.6cqi,30px)] font-extrabold tracking-[-0.02em] [text-shadow:var(--tth25-sx,0px)_var(--tth25-sy,5px)_var(--tth25-sb,9px)_color-mix(in_oklch,var(--color-tth25-sh)_38%,transparent)] transition-[text-shadow] duration-80 ease-linear">Umbra</h3><p class="text-[13.5px] leading-[1.6] text-tth25-mut">The hard core of a shadow — short and sharp when light is near.</p></a>
<a href="#tth25-top" data-tth25-lit class="flex flex-col gap-2 rounded-2xl border border-tth25-line bg-tth25-card p-[clamp(16px,3cqi,24px)] no-underline shadow-[var(--tth25-sx,0px)_var(--tth25-sy,6px)_calc(var(--tth25-sb,9px)*2.4)_-6px_color-mix(in_oklch,var(--color-tth25-sh)_22%,transparent)] transition-[border-color,box-shadow] duration-80 hover:border-tth25-ink focus-visible:border-tth25-ink focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tth25-ink"><h3 data-tth25-lit class="text-[clamp(22px,3.6cqi,30px)] font-extrabold tracking-[-0.02em] [text-shadow:var(--tth25-sx,0px)_var(--tth25-sy,5px)_var(--tth25-sb,9px)_color-mix(in_oklch,var(--color-tth25-sh)_38%,transparent)] transition-[text-shadow] duration-80 ease-linear">Penumbra</h3><p class="text-[13.5px] leading-[1.6] text-tth25-mut">The soft fringe — watch blur climb as the lamp walks away.</p></a>
<a href="#tth25-top" data-tth25-lit class="flex flex-col gap-2 rounded-2xl border border-tth25-line bg-tth25-card p-[clamp(16px,3cqi,24px)] no-underline shadow-[var(--tth25-sx,0px)_var(--tth25-sy,6px)_calc(var(--tth25-sb,9px)*2.4)_-6px_color-mix(in_oklch,var(--color-tth25-sh)_22%,transparent)] transition-[border-color,box-shadow] duration-80 hover:border-tth25-ink focus-visible:border-tth25-ink focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-tth25-ink"><h3 data-tth25-lit class="text-[clamp(22px,3.6cqi,30px)] font-extrabold tracking-[-0.02em] [text-shadow:var(--tth25-sx,0px)_var(--tth25-sy,5px)_var(--tth25-sb,9px)_color-mix(in_oklch,var(--color-tth25-sh)_38%,transparent)] transition-[text-shadow] duration-80 ease-linear">Cast</h3><p class="text-[13.5px] leading-[1.6] text-tth25-mut">Direction disagrees per card — that's what sells one shared lamp.</p></a>
</div>
<p id="tth25-top" class="text-center text-[12.5px] text-tth25-mut">JS writes <code class="rounded bg-tth25-chip px-1.5 py-px font-tth25-mono text-[0.9em]">--tth25-sx/sy/sb</code> per element; Tailwind owns the shadows. No pointer? The fallback values <em>are</em> the resting design.</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tth25-bg: #f4f1ea;
--color-tth25-bg: oklch(0.95 0.008 85);
--color-tth25-card: #fcfbf8;
--color-tth25-card: oklch(0.985 0.005 85);
--color-tth25-chip: #e6e2d8;
--color-tth25-chip: oklch(0.9 0.01 85);
--color-tth25-ink: #1f1e2e;
--color-tth25-ink: oklch(0.24 0.02 275);
--color-tth25-mut: #5b5a6a;
--color-tth25-mut: oklch(0.47 0.02 275);
--color-tth25-line: #ddd8cc;
--color-tth25-line: oklch(0.88 0.012 85);
--color-tth25-sh: #252a3a;
--color-tth25-sh: oklch(0.25 0.03 265);
--font-tth25-sans: 'Bricolage Grotesque', system-ui, sans-serif;
--font-tth25-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tth-25 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(function () {
var board = document.querySelector('[data-tth25-board]');
if (!board) return;
var targets = [].slice.call(board.querySelectorAll('[data-tth25-lit]'));
var rects = [];
function measure() { rects = targets.map(function (el) { return el.getBoundingClientRect(); }); }
measure();
addEventListener('resize', measure);
addEventListener('scroll', measure, true);
board.addEventListener('pointermove', function (e) {
var br = board.getBoundingClientRect();
board.style.setProperty('--tth25-lx', ((e.clientX - br.left) / br.width * 100).toFixed(1) + '%');
board.style.setProperty('--tth25-ly', ((e.clientY - br.top) / br.height * 100).toFixed(1) + '%');
targets.forEach(function (el, i) {
var r = rects[i] || el.getBoundingClientRect();
var dx = (r.left + r.width / 2) - e.clientX, dy = (r.top + r.height / 2) - e.clientY;
var d = Math.hypot(dx, dy) || 1, pull = Math.min(d / 14, 18);
el.style.setProperty('--tth25-sx', (dx / d * pull).toFixed(1) + 'px');
el.style.setProperty('--tth25-sy', (dy / d * pull).toFixed(1) + 'px');
el.style.setProperty('--tth25-sb', (2 + pull * 1.1).toFixed(1) + 'px');
});
});
})();
```How this works
One light, many shadows. On every pointermove the script computes, for each [data-tth25-lit] element, the vector from the cursor to the element's centre. The shadow points along that vector; its length and blur grow with distance (capped):
var d = Math.hypot(dx, dy) || 1, pull = Math.min(d / 14, 18);
el.style.setProperty('--tth25-sx', (dx / d * pull) + 'px');Why per element. Three cards around one cursor get three different directions — that disagreement is what convinces the eye there's a single, local light.
Cached rects. Element positions are measured once (and on resize/scroll), not on every move, so the handler only does arithmetic.
Make it yours
- Shadow colour:
--color-tth25-sh; warmer lamps look great with a brown shadow. - Max length: the
18cap inpull. - Remove the light pool: drop the radial gradient from the stage background.
Gotchas — read before shipping
- Inherited custom properties: the card's
--tth25-*would leak into its heading — both carrydata-tth25-lit, so each gets its own values. - Always ship
var()fallbacks — they render before the first move and on touch screens. - Throttle isn't needed at this cost, but avoid reading layout inside
pointermove.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Plain DOM APIs + container queries; inside v4's floor.