26 / 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 Infinite Text Marquee that Speeds Up on Hover
Streetwear ticker tapes in counter-flow: two tilted strips loop with animate-tth26-roll and its reverse twin. Hovering a strip surges it to 4× with updatePlaybackRate(4) — no phase jump, which a CSS duration swap can't do. A real aria-pressed pause button styles itself with aria-pressed: and starts pressed for reduced-motion users.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tth-26 @container w-full min-h-svh grid place-items-center overflow-clip bg-tth26-bg py-14 font-tth26-sans text-tth26-ink" aria-label="Marquee speed up on hover demo">
<div class="flex w-full flex-col gap-[clamp(22px,4cqi,36px)]">
<header class="flex items-center justify-between gap-4 px-[clamp(20px,5cqi,64px)]">
<h2 class="text-[clamp(30px,6cqi,54px)] font-black tracking-[-0.04em] [font-stretch:110%]">ARCHIVE / 004</h2>
<button type="button" data-tth26-pause aria-pressed="false" class="group/pause inline-flex min-h-[46px] cursor-pointer items-center gap-2.5 rounded-full border-2 border-tth26-ink bg-tth26-paper px-5 text-[13px] font-extrabold uppercase tracking-[0.06em] transition-colors duration-250 hover:bg-tth26-ink hover:text-tth26-paper aria-pressed:border-tth26-acc aria-pressed:bg-tth26-acc aria-pressed:text-tth26-paper focus-visible:outline-3 focus-visible:outline-offset-[3px] focus-visible:outline-tth26-acc"><span aria-hidden="true" class="flex gap-[3px] group-aria-pressed/pause:hidden"><i class="h-3 w-[3px] bg-current"></i><i class="h-3 w-[3px] bg-current"></i></span><span aria-hidden="true" class="hidden size-0 border-y-[6px] border-l-[9px] border-y-transparent border-l-current group-aria-pressed/pause:block"></span><span data-tth26-label>Pause tickers</span></button>
</header>
<div id="tth26-top" class="-rotate-[1.6deg] scale-x-[1.02] cursor-pointer overflow-clip bg-tth26-ink py-3.5 text-tth26-paper">
<div data-tth26-track class="flex w-max animate-tth26-roll">
<p class="whitespace-nowrap pr-[0.5em] text-[clamp(17px,3cqi,26px)] font-extrabold tracking-[0.04em]">NEW DROP 004 ◆ FREE SHIPPING OVER $120 ◆ STORE OPENS FRI 10:00 ◆ NO RESTOCKS ◆ </p>
<p aria-hidden="true" class="whitespace-nowrap pr-[0.5em] text-[clamp(17px,3cqi,26px)] font-extrabold tracking-[0.04em]">NEW DROP 004 ◆ FREE SHIPPING OVER $120 ◆ STORE OPENS FRI 10:00 ◆ NO RESTOCKS ◆ </p>
</div>
</div>
<div class="-mt-1 rotate-[1.2deg] scale-x-[1.02] cursor-pointer overflow-clip bg-tth26-acc py-3.5 text-tth26-paper">
<div data-tth26-track class="flex w-max animate-tth26-roll-rev">
<p class="whitespace-nowrap pr-[0.5em] text-[clamp(17px,3cqi,26px)] font-extrabold tracking-[0.04em]">WORLDWIDE ◇ 100% RECYCLED FLEECE ◇ SIZE RUNS BIG ◇ MEMBERS EARLY ACCESS THU ◇ </p>
<p aria-hidden="true" class="whitespace-nowrap pr-[0.5em] text-[clamp(17px,3cqi,26px)] font-extrabold tracking-[0.04em]">WORLDWIDE ◇ 100% RECYCLED FLEECE ◇ SIZE RUNS BIG ◇ MEMBERS EARLY ACCESS THU ◇ </p>
</div>
</div>
<p class="max-w-[64ch] px-[clamp(20px,5cqi,64px)] text-[12.5px] leading-[1.7] text-tth26-mut">Hover a strip: <code class="rounded bg-tth26-chip px-1.5 py-px font-tth26-mono text-[0.9em]">anim.updatePlaybackRate(4)</code> surges it with zero phase jump — the thing a CSS duration swap can't do. Counter-flow directions are the streetwear tell.</p>
</div>
</section><section class="tth-26 @container w-full min-h-svh grid place-items-center overflow-clip bg-tth26-bg py-14 font-tth26-sans text-tth26-ink" aria-label="Marquee speed up on hover demo">
<div class="flex w-full flex-col gap-[clamp(22px,4cqi,36px)]">
<header class="flex items-center justify-between gap-4 px-[clamp(20px,5cqi,64px)]">
<h2 class="text-[clamp(30px,6cqi,54px)] font-black tracking-[-0.04em] [font-stretch:110%]">ARCHIVE / 004</h2>
<button type="button" data-tth26-pause aria-pressed="false" class="group/pause inline-flex min-h-[46px] cursor-pointer items-center gap-2.5 rounded-full border-2 border-tth26-ink bg-tth26-paper px-5 text-[13px] font-extrabold uppercase tracking-[0.06em] transition-colors duration-250 hover:bg-tth26-ink hover:text-tth26-paper aria-pressed:border-tth26-acc aria-pressed:bg-tth26-acc aria-pressed:text-tth26-paper focus-visible:outline-3 focus-visible:outline-offset-[3px] focus-visible:outline-tth26-acc"><span aria-hidden="true" class="flex gap-[3px] group-aria-pressed/pause:hidden"><i class="h-3 w-[3px] bg-current"></i><i class="h-3 w-[3px] bg-current"></i></span><span aria-hidden="true" class="hidden size-0 border-y-[6px] border-l-[9px] border-y-transparent border-l-current group-aria-pressed/pause:block"></span><span data-tth26-label>Pause tickers</span></button>
</header>
<div id="tth26-top" class="-rotate-[1.6deg] scale-x-[1.02] cursor-pointer overflow-clip bg-tth26-ink py-3.5 text-tth26-paper">
<div data-tth26-track class="flex w-max animate-tth26-roll">
<p class="whitespace-nowrap pr-[0.5em] text-[clamp(17px,3cqi,26px)] font-extrabold tracking-[0.04em]">NEW DROP 004 ◆ FREE SHIPPING OVER $120 ◆ STORE OPENS FRI 10:00 ◆ NO RESTOCKS ◆ </p>
<p aria-hidden="true" class="whitespace-nowrap pr-[0.5em] text-[clamp(17px,3cqi,26px)] font-extrabold tracking-[0.04em]">NEW DROP 004 ◆ FREE SHIPPING OVER $120 ◆ STORE OPENS FRI 10:00 ◆ NO RESTOCKS ◆ </p>
</div>
</div>
<div class="-mt-1 rotate-[1.2deg] scale-x-[1.02] cursor-pointer overflow-clip bg-tth26-acc py-3.5 text-tth26-paper">
<div data-tth26-track class="flex w-max animate-tth26-roll-rev">
<p class="whitespace-nowrap pr-[0.5em] text-[clamp(17px,3cqi,26px)] font-extrabold tracking-[0.04em]">WORLDWIDE ◇ 100% RECYCLED FLEECE ◇ SIZE RUNS BIG ◇ MEMBERS EARLY ACCESS THU ◇ </p>
<p aria-hidden="true" class="whitespace-nowrap pr-[0.5em] text-[clamp(17px,3cqi,26px)] font-extrabold tracking-[0.04em]">WORLDWIDE ◇ 100% RECYCLED FLEECE ◇ SIZE RUNS BIG ◇ MEMBERS EARLY ACCESS THU ◇ </p>
</div>
</div>
<p class="max-w-[64ch] px-[clamp(20px,5cqi,64px)] text-[12.5px] leading-[1.7] text-tth26-mut">Hover a strip: <code class="rounded bg-tth26-chip px-1.5 py-px font-tth26-mono text-[0.9em]">anim.updatePlaybackRate(4)</code> surges it with zero phase jump — the thing a CSS duration swap can't do. Counter-flow directions are the streetwear tell.</p>
</div>
</section>@import "tailwindcss";
@theme {
--color-tth26-bg: #f5f4f0;
--color-tth26-bg: oklch(0.96 0.005 90);
--color-tth26-chip: #e3e2dd;
--color-tth26-chip: oklch(0.9 0.008 90);
--color-tth26-ink: #121210;
--color-tth26-ink: oklch(0.17 0.005 90);
--color-tth26-paper: #fdfdfb;
--color-tth26-paper: oklch(0.99 0.002 90);
--color-tth26-mut: #5f5e59;
--color-tth26-mut: oklch(0.46 0.01 90);
--color-tth26-acc: #d63a1f;
--color-tth26-acc: oklch(0.58 0.21 30);
--font-tth26-sans: 'Archivo', system-ui, sans-serif;
--font-tth26-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-tth26-roll: tth26-roll 16s linear infinite;
--animate-tth26-roll-rev: tth26-roll 22s linear infinite reverse;
@keyframes tth26-roll {
to {
transform: translateX(-50%);
}
}
}
.tth-26 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tth26-bg: #f5f4f0;
--color-tth26-bg: oklch(0.96 0.005 90);
--color-tth26-chip: #e3e2dd;
--color-tth26-chip: oklch(0.9 0.008 90);
--color-tth26-ink: #121210;
--color-tth26-ink: oklch(0.17 0.005 90);
--color-tth26-paper: #fdfdfb;
--color-tth26-paper: oklch(0.99 0.002 90);
--color-tth26-mut: #5f5e59;
--color-tth26-mut: oklch(0.46 0.01 90);
--color-tth26-acc: #d63a1f;
--color-tth26-acc: oklch(0.58 0.21 30);
--font-tth26-sans: 'Archivo', system-ui, sans-serif;
--font-tth26-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-tth26-roll: tth26-roll 16s linear infinite;
--animate-tth26-roll-rev: tth26-roll 22s linear infinite reverse;
@keyframes tth26-roll {
to {
transform: translateX(-50%);
}
}
}
.tth-26 {
width: 100%;
min-height: 100svh;
}(function () {
var reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
var tracks = [].slice.call(document.querySelectorAll('[data-tth26-track]'));
var anims = tracks.map(function (t) {
return (t.getAnimations ? t.getAnimations() : []).filter(function (x) { return x.animationName === 'tth26-roll'; })[0];
});
tracks.forEach(function (t, i) {
var a = anims[i]; if (!a) return;
t.parentElement.addEventListener('pointerenter', function () { a.updatePlaybackRate(4); });
t.parentElement.addEventListener('pointerleave', function () { a.updatePlaybackRate(1); });
});
var btn = document.querySelector('[data-tth26-pause]'), label = btn && btn.querySelector('[data-tth26-label]');
function set(pause) {
btn.setAttribute('aria-pressed', String(pause));
label.textContent = pause ? 'Play tickers' : 'Pause tickers';
anims.forEach(function (a) { if (a) pause ? a.pause() : a.play(); });
}
if (btn) { set(reduced); btn.addEventListener('click', function () { set(btn.getAttribute('aria-pressed') !== 'true'); }); }
})();(function () {
var reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
var tracks = [].slice.call(document.querySelectorAll('[data-tth26-track]'));
var anims = tracks.map(function (t) {
return (t.getAnimations ? t.getAnimations() : []).filter(function (x) { return x.animationName === 'tth26-roll'; })[0];
});
tracks.forEach(function (t, i) {
var a = anims[i]; if (!a) return;
t.parentElement.addEventListener('pointerenter', function () { a.updatePlaybackRate(4); });
t.parentElement.addEventListener('pointerleave', function () { a.updatePlaybackRate(1); });
});
var btn = document.querySelector('[data-tth26-pause]'), label = btn && btn.querySelector('[data-tth26-label]');
function set(pause) {
btn.setAttribute('aria-pressed', String(pause));
label.textContent = pause ? 'Play tickers' : 'Pause tickers';
anims.forEach(function (a) { if (a) pause ? a.pause() : a.play(); });
}
if (btn) { set(reduced); btn.addEventListener('click', function () { set(btn.getAttribute('aria-pressed') !== 'true'); }); }
})();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-tth26-bg: #f5f4f0;
--color-tth26-bg: oklch(0.96 0.005 90);
--color-tth26-chip: #e3e2dd;
--color-tth26-chip: oklch(0.9 0.008 90);
--color-tth26-ink: #121210;
--color-tth26-ink: oklch(0.17 0.005 90);
--color-tth26-paper: #fdfdfb;
--color-tth26-paper: oklch(0.99 0.002 90);
--color-tth26-mut: #5f5e59;
--color-tth26-mut: oklch(0.46 0.01 90);
--color-tth26-acc: #d63a1f;
--color-tth26-acc: oklch(0.58 0.21 30);
--font-tth26-sans: 'Archivo', system-ui, sans-serif;
--font-tth26-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-tth26-roll: tth26-roll 16s linear infinite;
--animate-tth26-roll-rev: tth26-roll 22s linear infinite reverse;
@keyframes tth26-roll {
to {
transform: translateX(-50%);
}
}
}
.tth-26 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tth26-bg: #f5f4f0;
--color-tth26-bg: oklch(0.96 0.005 90);
--color-tth26-chip: #e3e2dd;
--color-tth26-chip: oklch(0.9 0.008 90);
--color-tth26-ink: #121210;
--color-tth26-ink: oklch(0.17 0.005 90);
--color-tth26-paper: #fdfdfb;
--color-tth26-paper: oklch(0.99 0.002 90);
--color-tth26-mut: #5f5e59;
--color-tth26-mut: oklch(0.46 0.01 90);
--color-tth26-acc: #d63a1f;
--color-tth26-acc: oklch(0.58 0.21 30);
--font-tth26-sans: 'Archivo', system-ui, sans-serif;
--font-tth26-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-tth26-roll: tth26-roll 16s linear infinite;
--animate-tth26-roll-rev: tth26-roll 22s linear infinite reverse;
@keyframes tth26-roll {
to {
transform: translateX(-50%);
}
}
}
.tth-26 {
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=Archivo:wdth,wght@62..125,700..900&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=Archivo:wdth,wght@62..125,700..900&family=JetBrains+Mono:wght@400;600&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The
.track/.track--revanimation rules became two--animate-*tokens sharing one keyframe; the button's[aria-pressed="true"]rule becamearia-pressed:variants. - The script selects
[data-tth26-track]/[data-tth26-pause]— no BEM classes in JS. - UI upgrade from the CSS original: under reduced motion the tickers were paused but the button still said “Pause tickers” — the opposite of the truth. It now starts pressed (“Play tickers”), so state and label always agree.
- UI upgrade: the button gains pause/play glyphs driven by
group-aria-pressed/pause:, and Archivo Expanded gives the strips real streetwear width.
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 Infinite Text Marquee that Speeds Up on Hover
Source: https://codefronts.com/motion/tailwind-css-text-hover-effects/tailwind-infinite-text-marquee-that-speeds-up-on-hover/
Streetwear ticker tapes in counter-flow: two tilted strips loop with animate-tth26-roll and its reverse twin. Hovering a strip surges it to 4× with updatePlaybackRate(4) — no phase jump, which a CSS duration swap can't do. A real aria-pressed pause button styles itself with aria-pressed: and starts pressed for reduced-motion users.
## HTML
```html
<section class="tth-26 @container w-full min-h-svh grid place-items-center overflow-clip bg-tth26-bg py-14 font-tth26-sans text-tth26-ink" aria-label="Marquee speed up on hover demo">
<div class="flex w-full flex-col gap-[clamp(22px,4cqi,36px)]">
<header class="flex items-center justify-between gap-4 px-[clamp(20px,5cqi,64px)]">
<h2 class="text-[clamp(30px,6cqi,54px)] font-black tracking-[-0.04em] [font-stretch:110%]">ARCHIVE / 004</h2>
<button type="button" data-tth26-pause aria-pressed="false" class="group/pause inline-flex min-h-[46px] cursor-pointer items-center gap-2.5 rounded-full border-2 border-tth26-ink bg-tth26-paper px-5 text-[13px] font-extrabold uppercase tracking-[0.06em] transition-colors duration-250 hover:bg-tth26-ink hover:text-tth26-paper aria-pressed:border-tth26-acc aria-pressed:bg-tth26-acc aria-pressed:text-tth26-paper focus-visible:outline-3 focus-visible:outline-offset-[3px] focus-visible:outline-tth26-acc"><span aria-hidden="true" class="flex gap-[3px] group-aria-pressed/pause:hidden"><i class="h-3 w-[3px] bg-current"></i><i class="h-3 w-[3px] bg-current"></i></span><span aria-hidden="true" class="hidden size-0 border-y-[6px] border-l-[9px] border-y-transparent border-l-current group-aria-pressed/pause:block"></span><span data-tth26-label>Pause tickers</span></button>
</header>
<div id="tth26-top" class="-rotate-[1.6deg] scale-x-[1.02] cursor-pointer overflow-clip bg-tth26-ink py-3.5 text-tth26-paper">
<div data-tth26-track class="flex w-max animate-tth26-roll">
<p class="whitespace-nowrap pr-[0.5em] text-[clamp(17px,3cqi,26px)] font-extrabold tracking-[0.04em]">NEW DROP 004 ◆ FREE SHIPPING OVER $120 ◆ STORE OPENS FRI 10:00 ◆ NO RESTOCKS ◆ </p>
<p aria-hidden="true" class="whitespace-nowrap pr-[0.5em] text-[clamp(17px,3cqi,26px)] font-extrabold tracking-[0.04em]">NEW DROP 004 ◆ FREE SHIPPING OVER $120 ◆ STORE OPENS FRI 10:00 ◆ NO RESTOCKS ◆ </p>
</div>
</div>
<div class="-mt-1 rotate-[1.2deg] scale-x-[1.02] cursor-pointer overflow-clip bg-tth26-acc py-3.5 text-tth26-paper">
<div data-tth26-track class="flex w-max animate-tth26-roll-rev">
<p class="whitespace-nowrap pr-[0.5em] text-[clamp(17px,3cqi,26px)] font-extrabold tracking-[0.04em]">WORLDWIDE ◇ 100% RECYCLED FLEECE ◇ SIZE RUNS BIG ◇ MEMBERS EARLY ACCESS THU ◇ </p>
<p aria-hidden="true" class="whitespace-nowrap pr-[0.5em] text-[clamp(17px,3cqi,26px)] font-extrabold tracking-[0.04em]">WORLDWIDE ◇ 100% RECYCLED FLEECE ◇ SIZE RUNS BIG ◇ MEMBERS EARLY ACCESS THU ◇ </p>
</div>
</div>
<p class="max-w-[64ch] px-[clamp(20px,5cqi,64px)] text-[12.5px] leading-[1.7] text-tth26-mut">Hover a strip: <code class="rounded bg-tth26-chip px-1.5 py-px font-tth26-mono text-[0.9em]">anim.updatePlaybackRate(4)</code> surges it with zero phase jump — the thing a CSS duration swap can't do. Counter-flow directions are the streetwear tell.</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tth26-bg: #f5f4f0;
--color-tth26-bg: oklch(0.96 0.005 90);
--color-tth26-chip: #e3e2dd;
--color-tth26-chip: oklch(0.9 0.008 90);
--color-tth26-ink: #121210;
--color-tth26-ink: oklch(0.17 0.005 90);
--color-tth26-paper: #fdfdfb;
--color-tth26-paper: oklch(0.99 0.002 90);
--color-tth26-mut: #5f5e59;
--color-tth26-mut: oklch(0.46 0.01 90);
--color-tth26-acc: #d63a1f;
--color-tth26-acc: oklch(0.58 0.21 30);
--font-tth26-sans: 'Archivo', system-ui, sans-serif;
--font-tth26-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-tth26-roll: tth26-roll 16s linear infinite;
--animate-tth26-roll-rev: tth26-roll 22s linear infinite reverse;
@keyframes tth26-roll {
to {
transform: translateX(-50%);
}
}
}
.tth-26 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(function () {
var reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
var tracks = [].slice.call(document.querySelectorAll('[data-tth26-track]'));
var anims = tracks.map(function (t) {
return (t.getAnimations ? t.getAnimations() : []).filter(function (x) { return x.animationName === 'tth26-roll'; })[0];
});
tracks.forEach(function (t, i) {
var a = anims[i]; if (!a) return;
t.parentElement.addEventListener('pointerenter', function () { a.updatePlaybackRate(4); });
t.parentElement.addEventListener('pointerleave', function () { a.updatePlaybackRate(1); });
});
var btn = document.querySelector('[data-tth26-pause]'), label = btn && btn.querySelector('[data-tth26-label]');
function set(pause) {
btn.setAttribute('aria-pressed', String(pause));
label.textContent = pause ? 'Play tickers' : 'Pause tickers';
anims.forEach(function (a) { if (a) pause ? a.pause() : a.play(); });
}
if (btn) { set(reduced); btn.addEventListener('click', function () { set(btn.getAttribute('aria-pressed') !== 'true'); }); }
})();
```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 Infinite Text Marquee that Speeds Up on Hover
Source: https://codefronts.com/motion/tailwind-css-text-hover-effects/tailwind-infinite-text-marquee-that-speeds-up-on-hover/
Streetwear ticker tapes in counter-flow: two tilted strips loop with animate-tth26-roll and its reverse twin. Hovering a strip surges it to 4× with updatePlaybackRate(4) — no phase jump, which a CSS duration swap can't do. A real aria-pressed pause button styles itself with aria-pressed: and starts pressed for reduced-motion users.
## HTML
```html
<section class="tth-26 @container w-full min-h-svh grid place-items-center overflow-clip bg-tth26-bg py-14 font-tth26-sans text-tth26-ink" aria-label="Marquee speed up on hover demo">
<div class="flex w-full flex-col gap-[clamp(22px,4cqi,36px)]">
<header class="flex items-center justify-between gap-4 px-[clamp(20px,5cqi,64px)]">
<h2 class="text-[clamp(30px,6cqi,54px)] font-black tracking-[-0.04em] [font-stretch:110%]">ARCHIVE / 004</h2>
<button type="button" data-tth26-pause aria-pressed="false" class="group/pause inline-flex min-h-[46px] cursor-pointer items-center gap-2.5 rounded-full border-2 border-tth26-ink bg-tth26-paper px-5 text-[13px] font-extrabold uppercase tracking-[0.06em] transition-colors duration-250 hover:bg-tth26-ink hover:text-tth26-paper aria-pressed:border-tth26-acc aria-pressed:bg-tth26-acc aria-pressed:text-tth26-paper focus-visible:outline-3 focus-visible:outline-offset-[3px] focus-visible:outline-tth26-acc"><span aria-hidden="true" class="flex gap-[3px] group-aria-pressed/pause:hidden"><i class="h-3 w-[3px] bg-current"></i><i class="h-3 w-[3px] bg-current"></i></span><span aria-hidden="true" class="hidden size-0 border-y-[6px] border-l-[9px] border-y-transparent border-l-current group-aria-pressed/pause:block"></span><span data-tth26-label>Pause tickers</span></button>
</header>
<div id="tth26-top" class="-rotate-[1.6deg] scale-x-[1.02] cursor-pointer overflow-clip bg-tth26-ink py-3.5 text-tth26-paper">
<div data-tth26-track class="flex w-max animate-tth26-roll">
<p class="whitespace-nowrap pr-[0.5em] text-[clamp(17px,3cqi,26px)] font-extrabold tracking-[0.04em]">NEW DROP 004 ◆ FREE SHIPPING OVER $120 ◆ STORE OPENS FRI 10:00 ◆ NO RESTOCKS ◆ </p>
<p aria-hidden="true" class="whitespace-nowrap pr-[0.5em] text-[clamp(17px,3cqi,26px)] font-extrabold tracking-[0.04em]">NEW DROP 004 ◆ FREE SHIPPING OVER $120 ◆ STORE OPENS FRI 10:00 ◆ NO RESTOCKS ◆ </p>
</div>
</div>
<div class="-mt-1 rotate-[1.2deg] scale-x-[1.02] cursor-pointer overflow-clip bg-tth26-acc py-3.5 text-tth26-paper">
<div data-tth26-track class="flex w-max animate-tth26-roll-rev">
<p class="whitespace-nowrap pr-[0.5em] text-[clamp(17px,3cqi,26px)] font-extrabold tracking-[0.04em]">WORLDWIDE ◇ 100% RECYCLED FLEECE ◇ SIZE RUNS BIG ◇ MEMBERS EARLY ACCESS THU ◇ </p>
<p aria-hidden="true" class="whitespace-nowrap pr-[0.5em] text-[clamp(17px,3cqi,26px)] font-extrabold tracking-[0.04em]">WORLDWIDE ◇ 100% RECYCLED FLEECE ◇ SIZE RUNS BIG ◇ MEMBERS EARLY ACCESS THU ◇ </p>
</div>
</div>
<p class="max-w-[64ch] px-[clamp(20px,5cqi,64px)] text-[12.5px] leading-[1.7] text-tth26-mut">Hover a strip: <code class="rounded bg-tth26-chip px-1.5 py-px font-tth26-mono text-[0.9em]">anim.updatePlaybackRate(4)</code> surges it with zero phase jump — the thing a CSS duration swap can't do. Counter-flow directions are the streetwear tell.</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tth26-bg: #f5f4f0;
--color-tth26-bg: oklch(0.96 0.005 90);
--color-tth26-chip: #e3e2dd;
--color-tth26-chip: oklch(0.9 0.008 90);
--color-tth26-ink: #121210;
--color-tth26-ink: oklch(0.17 0.005 90);
--color-tth26-paper: #fdfdfb;
--color-tth26-paper: oklch(0.99 0.002 90);
--color-tth26-mut: #5f5e59;
--color-tth26-mut: oklch(0.46 0.01 90);
--color-tth26-acc: #d63a1f;
--color-tth26-acc: oklch(0.58 0.21 30);
--font-tth26-sans: 'Archivo', system-ui, sans-serif;
--font-tth26-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-tth26-roll: tth26-roll 16s linear infinite;
--animate-tth26-roll-rev: tth26-roll 22s linear infinite reverse;
@keyframes tth26-roll {
to {
transform: translateX(-50%);
}
}
}
.tth-26 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(function () {
var reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
var tracks = [].slice.call(document.querySelectorAll('[data-tth26-track]'));
var anims = tracks.map(function (t) {
return (t.getAnimations ? t.getAnimations() : []).filter(function (x) { return x.animationName === 'tth26-roll'; })[0];
});
tracks.forEach(function (t, i) {
var a = anims[i]; if (!a) return;
t.parentElement.addEventListener('pointerenter', function () { a.updatePlaybackRate(4); });
t.parentElement.addEventListener('pointerleave', function () { a.updatePlaybackRate(1); });
});
var btn = document.querySelector('[data-tth26-pause]'), label = btn && btn.querySelector('[data-tth26-label]');
function set(pause) {
btn.setAttribute('aria-pressed', String(pause));
label.textContent = pause ? 'Play tickers' : 'Pause tickers';
anims.forEach(function (a) { if (a) pause ? a.pause() : a.play(); });
}
if (btn) { set(reduced); btn.addEventListener('click', function () { set(btn.getAttribute('aria-pressed') !== 'true'); }); }
})();
```How this works
Seamless loop. The track holds the message twice and is w-max wide. Animating it to translateX(-50%) moves exactly one copy's width; at the end the second copy sits where the first began, so the restart is invisible.
Why the Web Animations API. Changing animation-duration on hover recalculates progress from the new duration, so the strip jumps. The script grabs the running CSS animation object and changes only its playback rate:
var a = track.getAnimations().find(x => x.animationName === 'tth26-roll');
bar.addEventListener('pointerenter', () => a.updatePlaybackRate(4));Pause. WCAG 2.2.2 requires a way to stop moving content that runs longer than five seconds. The button toggles aria-pressed, which drives both its look (aria-pressed:) and the animations.
Make it yours
- Surge factor:
updatePlaybackRate(4); use0.25for a hover-to-slow reading mode. - Speeds:
16sand22sin the two--animate-*tokens. - Flat strips: drop
-rotate-[1.6deg]and the scale.
Gotchas — read before shipping
- The duplicate group must be
aria-hidden, or screen readers read the message twice. getAnimations()only finds the animation after styles compile — the script runs at the end of the page.- Rotated full-bleed strips need
scale-x-[1.02]and anoverflow-clipstage, or their corners show gaps.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
updatePlaybackRate() is supported in every modern engine.