02 / 30
All 30 in 30 Tailwind CSS Hover Effects
- 01Tailwind Underline Draw Hover Effect
- 02Tailwind Text Glitch Hover Effect
- 03Tailwind Letter Spacing Expand Hover Effect
- 04Tailwind Gradient Text Reveal Hover Effect
- 05Tailwind Split Text Hover Effect
- 06Tailwind Neon Glow Text Hover Effect
- 07Tailwind Magnetic Liquid Button Hover Effect
- 08Tailwind Border Draw Hover Effect
- 09Tailwind Shimmer Shine Button Hover Effect
- 10Tailwind Fill Wipe Button Hover Effect
- 11Tailwind 3D Press Button Hover Effect
- 12Tailwind Ripple Button Hover Effect
- 13Tailwind 3D Tilt Card Hover Effect
- 14Tailwind Card Flip Hover Effect
- 15Tailwind Glassmorphism Card Hover Effect
- 16Tailwind Spotlight Card Hover Effect
- 17Tailwind Slide Reveal Card Hover Effect
- 18Tailwind Stack Lift Card Hover Effect
- 19Tailwind Zoom Pan Image Hover Effect
- 20Tailwind Color Channel Split Hover Effect
- 21Tailwind Duotone Image Hover Effect
- 22Tailwind Curtain Reveal Image Hover Effect
- 23Tailwind Ken Burns Image Hover Effect
- 24Tailwind Image Tilt Depth Hover Effect
- 25Tailwind Underline Slide Nav Hover Effect
- 26Tailwind Highlight Fill Nav Hover Effect
- 27Tailwind Strikethrough Hover Link Effect
- 28Tailwind Inline Word Swap Hover Effect
- 29Tailwind Dot Trail Cursor Hover Effect
- 30Tailwind Magnetic Cursor Pull Hover Effect
Tailwind Text Glitch Hover Effect
Four glitch hovers — RGB channel split, scanline flicker, noise slice and VHS chromatic aberration. Keyframes stay in the stylesheet (they are motion definitions, not design-scale values), but each becomes an --animate-* token, so firing one is a single hover:before:animate-* utility.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="thv-02 w-full min-h-svh grid place-items-center bg-thv02-bg font-thv02-sans px-6 py-14">
<div class="w-[min(820px,100%)] grid grid-cols-[repeat(auto-fit,minmax(260px,1fr))] gap-10">
<div class="flex flex-col items-center gap-6 px-8 py-14 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<span tabindex="0" data-text="GLITCH" class="group relative inline-block text-[clamp(2rem,6vw,3.5rem)] font-black tracking-[0.08em] text-thv02-text rounded-xs focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-thv02-red before:content-[attr(data-text)] before:absolute before:inset-0 before:left-0.5 before:text-thv02-red before:pointer-events-none after:content-[attr(data-text)] after:absolute after:inset-0 after:-left-0.5 after:text-thv02-blue after:pointer-events-none hover:before:animate-thv02-rgb-b hover:after:animate-thv02-rgb-a focus-visible:before:animate-thv02-rgb-b focus-visible:after:animate-thv02-rgb-a motion-reduce:before:hidden motion-reduce:after:hidden">GLITCH</span>
<span class="text-[11px] tracking-[0.15em] uppercase text-thv02-dim">RGB Channel Split</span>
</div>
<div class="flex flex-col items-center gap-6 px-8 py-14 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<span tabindex="0" data-text="SCANLINE" class="relative inline-block text-[clamp(1.6rem,5vw,2.6rem)] font-black tracking-[0.08em] text-thv02-green rounded-xs focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-thv02-green before:content-[''] before:absolute before:inset-0 before:opacity-0 before:mix-blend-multiply before:transition-opacity before:duration-200 before:[background:repeating-linear-gradient(0deg,transparent,transparent_2px,rgba(0,0,0,.4)_2px,rgba(0,0,0,.4)_4px)] before:[background-size:100%_4px] after:content-[attr(data-text)] after:absolute after:inset-0 after:left-0.5 after:text-thv02-green after:opacity-0 after:transition-opacity after:duration-200 hover:before:opacity-100 hover:before:animate-thv02-scan hover:after:opacity-70 hover:after:animate-thv02-rgb-b focus-visible:before:opacity-100 focus-visible:after:opacity-70 motion-reduce:before:hidden motion-reduce:after:hidden">SCANLINE</span>
<span class="text-[11px] tracking-[0.15em] uppercase text-thv02-dim">Scanline Flicker</span>
</div>
<div class="flex flex-col items-center gap-6 px-8 py-14 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<span tabindex="0" data-text="NOISE" class="relative inline-block text-[clamp(2rem,6vw,3.5rem)] font-black tracking-[0.08em] text-thv02-yellow rounded-xs focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-thv02-yellow before:content-[attr(data-text)] before:absolute before:inset-0 before:text-thv02-red before:[clip-path:inset(0_0_100%_0)] after:content-[attr(data-text)] after:absolute after:inset-0 after:text-thv02-blue after:[clip-path:inset(100%_0_0_0)] hover:before:animate-thv02-slice-t hover:after:animate-thv02-slice-b focus-visible:before:animate-thv02-slice-t focus-visible:after:animate-thv02-slice-b motion-reduce:before:hidden motion-reduce:after:hidden">NOISE</span>
<span class="text-[11px] tracking-[0.15em] uppercase text-thv02-dim">Noise Slice</span>
</div>
<div class="flex flex-col items-center gap-6 px-8 py-14 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<span tabindex="0" data-text="VHS" class="relative inline-block text-[clamp(2rem,6vw,3.5rem)] font-black tracking-[0.08em] text-white rounded-xs focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-thv02-blue before:content-[attr(data-text)] before:absolute before:inset-0 before:text-thv02-red before:mix-blend-screen after:content-[attr(data-text)] after:absolute after:inset-0 after:text-thv02-blue after:mix-blend-screen hover:before:animate-thv02-vhs-r hover:after:animate-thv02-vhs-b focus-visible:before:animate-thv02-vhs-r focus-visible:after:animate-thv02-vhs-b motion-reduce:before:hidden motion-reduce:after:hidden">VHS</span>
<span class="text-[11px] tracking-[0.15em] uppercase text-thv02-dim">VHS Chromatic</span>
</div>
</div>
</section><section class="thv-02 w-full min-h-svh grid place-items-center bg-thv02-bg font-thv02-sans px-6 py-14">
<div class="w-[min(820px,100%)] grid grid-cols-[repeat(auto-fit,minmax(260px,1fr))] gap-10">
<div class="flex flex-col items-center gap-6 px-8 py-14 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<span tabindex="0" data-text="GLITCH" class="group relative inline-block text-[clamp(2rem,6vw,3.5rem)] font-black tracking-[0.08em] text-thv02-text rounded-xs focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-thv02-red before:content-[attr(data-text)] before:absolute before:inset-0 before:left-0.5 before:text-thv02-red before:pointer-events-none after:content-[attr(data-text)] after:absolute after:inset-0 after:-left-0.5 after:text-thv02-blue after:pointer-events-none hover:before:animate-thv02-rgb-b hover:after:animate-thv02-rgb-a focus-visible:before:animate-thv02-rgb-b focus-visible:after:animate-thv02-rgb-a motion-reduce:before:hidden motion-reduce:after:hidden">GLITCH</span>
<span class="text-[11px] tracking-[0.15em] uppercase text-thv02-dim">RGB Channel Split</span>
</div>
<div class="flex flex-col items-center gap-6 px-8 py-14 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<span tabindex="0" data-text="SCANLINE" class="relative inline-block text-[clamp(1.6rem,5vw,2.6rem)] font-black tracking-[0.08em] text-thv02-green rounded-xs focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-thv02-green before:content-[''] before:absolute before:inset-0 before:opacity-0 before:mix-blend-multiply before:transition-opacity before:duration-200 before:[background:repeating-linear-gradient(0deg,transparent,transparent_2px,rgba(0,0,0,.4)_2px,rgba(0,0,0,.4)_4px)] before:[background-size:100%_4px] after:content-[attr(data-text)] after:absolute after:inset-0 after:left-0.5 after:text-thv02-green after:opacity-0 after:transition-opacity after:duration-200 hover:before:opacity-100 hover:before:animate-thv02-scan hover:after:opacity-70 hover:after:animate-thv02-rgb-b focus-visible:before:opacity-100 focus-visible:after:opacity-70 motion-reduce:before:hidden motion-reduce:after:hidden">SCANLINE</span>
<span class="text-[11px] tracking-[0.15em] uppercase text-thv02-dim">Scanline Flicker</span>
</div>
<div class="flex flex-col items-center gap-6 px-8 py-14 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<span tabindex="0" data-text="NOISE" class="relative inline-block text-[clamp(2rem,6vw,3.5rem)] font-black tracking-[0.08em] text-thv02-yellow rounded-xs focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-thv02-yellow before:content-[attr(data-text)] before:absolute before:inset-0 before:text-thv02-red before:[clip-path:inset(0_0_100%_0)] after:content-[attr(data-text)] after:absolute after:inset-0 after:text-thv02-blue after:[clip-path:inset(100%_0_0_0)] hover:before:animate-thv02-slice-t hover:after:animate-thv02-slice-b focus-visible:before:animate-thv02-slice-t focus-visible:after:animate-thv02-slice-b motion-reduce:before:hidden motion-reduce:after:hidden">NOISE</span>
<span class="text-[11px] tracking-[0.15em] uppercase text-thv02-dim">Noise Slice</span>
</div>
<div class="flex flex-col items-center gap-6 px-8 py-14 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<span tabindex="0" data-text="VHS" class="relative inline-block text-[clamp(2rem,6vw,3.5rem)] font-black tracking-[0.08em] text-white rounded-xs focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-thv02-blue before:content-[attr(data-text)] before:absolute before:inset-0 before:text-thv02-red before:mix-blend-screen after:content-[attr(data-text)] after:absolute after:inset-0 after:text-thv02-blue after:mix-blend-screen hover:before:animate-thv02-vhs-r hover:after:animate-thv02-vhs-b focus-visible:before:animate-thv02-vhs-r focus-visible:after:animate-thv02-vhs-b motion-reduce:before:hidden motion-reduce:after:hidden">VHS</span>
<span class="text-[11px] tracking-[0.15em] uppercase text-thv02-dim">VHS Chromatic</span>
</div>
</div>
</section>@import "tailwindcss";
@theme {
--color-thv02-bg: #080810;
--color-thv02-bg: oklch(0.13 0.02 285);
--color-thv02-text: #f0f0f0;
--color-thv02-text: oklch(0.95 0 0);
--color-thv02-dim: #444444;
--color-thv02-dim: oklch(0.4 0 0);
--color-thv02-red: #ff003c;
--color-thv02-red: oklch(0.62 0.28 17);
--color-thv02-blue: #00e5ff;
--color-thv02-blue: oklch(0.83 0.15 203);
--color-thv02-green: #39ff14;
--color-thv02-green: oklch(0.88 0.29 135);
--color-thv02-yellow: #ffe600;
--color-thv02-yellow: oklch(0.91 0.2 103);
--font-thv02-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-thv02-rgb-b: thv02-rgb-b .4s steps(2) infinite;
--animate-thv02-rgb-a: thv02-rgb-a .4s steps(2) .1s infinite;
--animate-thv02-scan: thv02-scan .8s linear infinite;
--animate-thv02-slice-t: thv02-slice-t .5s steps(4) infinite;
--animate-thv02-slice-b: thv02-slice-b .5s steps(4) .15s infinite;
--animate-thv02-vhs-r: thv02-vhs-r .6s steps(3) infinite;
--animate-thv02-vhs-b: thv02-vhs-b .6s steps(3) .2s infinite;
}
.thv-02 {
width: 100%;
min-height: 100svh;
}
.thv-02 ::selection {
background: var(--color-thv02-red);
color: #fff;
}
/* Motion definitions stay in CSS; @theme above mints the animate-* utilities. */
@keyframes thv02-rgb-b {
0% {
clip-path: inset(0 0 95% 0);
transform: translateX(-3px);
}
20% {
clip-path: inset(30% 0 50% 0);
transform: translateX(3px);
}
40% {
clip-path: inset(60% 0 20% 0);
transform: translateX(-2px);
}
60% {
clip-path: inset(10% 0 75% 0);
transform: translateX(4px);
}
80% {
clip-path: inset(80% 0 5% 0);
transform: translateX(-3px);
}
100% {
clip-path: inset(45% 0 40% 0);
transform: translateX(2px);
}
}
@keyframes thv02-rgb-a {
0% {
clip-path: inset(50% 0 30% 0);
transform: translateX(3px);
}
25% {
clip-path: inset(5% 0 80% 0);
transform: translateX(-4px);
}
50% {
clip-path: inset(70% 0 15% 0);
transform: translateX(2px);
}
75% {
clip-path: inset(20% 0 60% 0);
transform: translateX(-3px);
}
100% {
clip-path: inset(40% 0 45% 0);
transform: translateX(3px);
}
}
@keyframes thv02-scan {
0% {
background-position-y: 0;
}
100% {
background-position-y: 100px;
}
}
@keyframes thv02-slice-t {
0% {
clip-path: inset(0 0 85% 0);
transform: translateX(6px) skewX(-2deg);
}
33% {
clip-path: inset(25% 0 55% 0);
transform: translateX(-5px);
}
66% {
clip-path: inset(60% 0 10% 0);
transform: translateX(4px) skewX(1deg);
}
100% {
clip-path: inset(10% 0 75% 0);
transform: translateX(-6px);
}
}
@keyframes thv02-slice-b {
0% {
clip-path: inset(80% 0 0 0);
transform: translateX(-4px);
}
33% {
clip-path: inset(50% 0 20% 0);
transform: translateX(6px);
}
66% {
clip-path: inset(15% 0 70% 0);
transform: translateX(-3px) skewX(2deg);
}
100% {
clip-path: inset(65% 0 5% 0);
transform: translateX(5px);
}
}
@keyframes thv02-vhs-r {
0% {
transform: translateX(-6px);
clip-path: inset(20% 0 60% 0);
}
50% {
transform: translateX(4px);
clip-path: inset(55% 0 10% 0);
}
100% {
transform: translateX(-5px);
clip-path: inset(5% 0 80% 0);
}
}
@keyframes thv02-vhs-b {
0% {
transform: translateX(6px);
clip-path: inset(40% 0 30% 0);
}
50% {
transform: translateX(-4px);
clip-path: inset(10% 0 65% 0);
}
100% {
transform: translateX(5px);
clip-path: inset(70% 0 5% 0);
}
}@import "tailwindcss";
@theme {
--color-thv02-bg: #080810;
--color-thv02-bg: oklch(0.13 0.02 285);
--color-thv02-text: #f0f0f0;
--color-thv02-text: oklch(0.95 0 0);
--color-thv02-dim: #444444;
--color-thv02-dim: oklch(0.4 0 0);
--color-thv02-red: #ff003c;
--color-thv02-red: oklch(0.62 0.28 17);
--color-thv02-blue: #00e5ff;
--color-thv02-blue: oklch(0.83 0.15 203);
--color-thv02-green: #39ff14;
--color-thv02-green: oklch(0.88 0.29 135);
--color-thv02-yellow: #ffe600;
--color-thv02-yellow: oklch(0.91 0.2 103);
--font-thv02-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-thv02-rgb-b: thv02-rgb-b .4s steps(2) infinite;
--animate-thv02-rgb-a: thv02-rgb-a .4s steps(2) .1s infinite;
--animate-thv02-scan: thv02-scan .8s linear infinite;
--animate-thv02-slice-t: thv02-slice-t .5s steps(4) infinite;
--animate-thv02-slice-b: thv02-slice-b .5s steps(4) .15s infinite;
--animate-thv02-vhs-r: thv02-vhs-r .6s steps(3) infinite;
--animate-thv02-vhs-b: thv02-vhs-b .6s steps(3) .2s infinite;
}
.thv-02 {
width: 100%;
min-height: 100svh;
}
.thv-02 ::selection {
background: var(--color-thv02-red);
color: #fff;
}
/* Motion definitions stay in CSS; @theme above mints the animate-* utilities. */
@keyframes thv02-rgb-b {
0% {
clip-path: inset(0 0 95% 0);
transform: translateX(-3px);
}
20% {
clip-path: inset(30% 0 50% 0);
transform: translateX(3px);
}
40% {
clip-path: inset(60% 0 20% 0);
transform: translateX(-2px);
}
60% {
clip-path: inset(10% 0 75% 0);
transform: translateX(4px);
}
80% {
clip-path: inset(80% 0 5% 0);
transform: translateX(-3px);
}
100% {
clip-path: inset(45% 0 40% 0);
transform: translateX(2px);
}
}
@keyframes thv02-rgb-a {
0% {
clip-path: inset(50% 0 30% 0);
transform: translateX(3px);
}
25% {
clip-path: inset(5% 0 80% 0);
transform: translateX(-4px);
}
50% {
clip-path: inset(70% 0 15% 0);
transform: translateX(2px);
}
75% {
clip-path: inset(20% 0 60% 0);
transform: translateX(-3px);
}
100% {
clip-path: inset(40% 0 45% 0);
transform: translateX(3px);
}
}
@keyframes thv02-scan {
0% {
background-position-y: 0;
}
100% {
background-position-y: 100px;
}
}
@keyframes thv02-slice-t {
0% {
clip-path: inset(0 0 85% 0);
transform: translateX(6px) skewX(-2deg);
}
33% {
clip-path: inset(25% 0 55% 0);
transform: translateX(-5px);
}
66% {
clip-path: inset(60% 0 10% 0);
transform: translateX(4px) skewX(1deg);
}
100% {
clip-path: inset(10% 0 75% 0);
transform: translateX(-6px);
}
}
@keyframes thv02-slice-b {
0% {
clip-path: inset(80% 0 0 0);
transform: translateX(-4px);
}
33% {
clip-path: inset(50% 0 20% 0);
transform: translateX(6px);
}
66% {
clip-path: inset(15% 0 70% 0);
transform: translateX(-3px) skewX(2deg);
}
100% {
clip-path: inset(65% 0 5% 0);
transform: translateX(5px);
}
}
@keyframes thv02-vhs-r {
0% {
transform: translateX(-6px);
clip-path: inset(20% 0 60% 0);
}
50% {
transform: translateX(4px);
clip-path: inset(55% 0 10% 0);
}
100% {
transform: translateX(-5px);
clip-path: inset(5% 0 80% 0);
}
}
@keyframes thv02-vhs-b {
0% {
transform: translateX(6px);
clip-path: inset(40% 0 30% 0);
}
50% {
transform: translateX(-4px);
clip-path: inset(10% 0 65% 0);
}
100% {
transform: translateX(5px);
clip-path: inset(70% 0 5% 0);
}
}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-thv02-bg: #080810;
--color-thv02-bg: oklch(0.13 0.02 285);
--color-thv02-text: #f0f0f0;
--color-thv02-text: oklch(0.95 0 0);
--color-thv02-dim: #444444;
--color-thv02-dim: oklch(0.4 0 0);
--color-thv02-red: #ff003c;
--color-thv02-red: oklch(0.62 0.28 17);
--color-thv02-blue: #00e5ff;
--color-thv02-blue: oklch(0.83 0.15 203);
--color-thv02-green: #39ff14;
--color-thv02-green: oklch(0.88 0.29 135);
--color-thv02-yellow: #ffe600;
--color-thv02-yellow: oklch(0.91 0.2 103);
--font-thv02-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-thv02-rgb-b: thv02-rgb-b .4s steps(2) infinite;
--animate-thv02-rgb-a: thv02-rgb-a .4s steps(2) .1s infinite;
--animate-thv02-scan: thv02-scan .8s linear infinite;
--animate-thv02-slice-t: thv02-slice-t .5s steps(4) infinite;
--animate-thv02-slice-b: thv02-slice-b .5s steps(4) .15s infinite;
--animate-thv02-vhs-r: thv02-vhs-r .6s steps(3) infinite;
--animate-thv02-vhs-b: thv02-vhs-b .6s steps(3) .2s infinite;
}
.thv-02 {
width: 100%;
min-height: 100svh;
}
.thv-02 ::selection {
background: var(--color-thv02-red);
color: #fff;
}
/* Motion definitions stay in CSS; @theme above mints the animate-* utilities. */
@keyframes thv02-rgb-b {
0% {
clip-path: inset(0 0 95% 0);
transform: translateX(-3px);
}
20% {
clip-path: inset(30% 0 50% 0);
transform: translateX(3px);
}
40% {
clip-path: inset(60% 0 20% 0);
transform: translateX(-2px);
}
60% {
clip-path: inset(10% 0 75% 0);
transform: translateX(4px);
}
80% {
clip-path: inset(80% 0 5% 0);
transform: translateX(-3px);
}
100% {
clip-path: inset(45% 0 40% 0);
transform: translateX(2px);
}
}
@keyframes thv02-rgb-a {
0% {
clip-path: inset(50% 0 30% 0);
transform: translateX(3px);
}
25% {
clip-path: inset(5% 0 80% 0);
transform: translateX(-4px);
}
50% {
clip-path: inset(70% 0 15% 0);
transform: translateX(2px);
}
75% {
clip-path: inset(20% 0 60% 0);
transform: translateX(-3px);
}
100% {
clip-path: inset(40% 0 45% 0);
transform: translateX(3px);
}
}
@keyframes thv02-scan {
0% {
background-position-y: 0;
}
100% {
background-position-y: 100px;
}
}
@keyframes thv02-slice-t {
0% {
clip-path: inset(0 0 85% 0);
transform: translateX(6px) skewX(-2deg);
}
33% {
clip-path: inset(25% 0 55% 0);
transform: translateX(-5px);
}
66% {
clip-path: inset(60% 0 10% 0);
transform: translateX(4px) skewX(1deg);
}
100% {
clip-path: inset(10% 0 75% 0);
transform: translateX(-6px);
}
}
@keyframes thv02-slice-b {
0% {
clip-path: inset(80% 0 0 0);
transform: translateX(-4px);
}
33% {
clip-path: inset(50% 0 20% 0);
transform: translateX(6px);
}
66% {
clip-path: inset(15% 0 70% 0);
transform: translateX(-3px) skewX(2deg);
}
100% {
clip-path: inset(65% 0 5% 0);
transform: translateX(5px);
}
}
@keyframes thv02-vhs-r {
0% {
transform: translateX(-6px);
clip-path: inset(20% 0 60% 0);
}
50% {
transform: translateX(4px);
clip-path: inset(55% 0 10% 0);
}
100% {
transform: translateX(-5px);
clip-path: inset(5% 0 80% 0);
}
}
@keyframes thv02-vhs-b {
0% {
transform: translateX(6px);
clip-path: inset(40% 0 30% 0);
}
50% {
transform: translateX(-4px);
clip-path: inset(10% 0 65% 0);
}
100% {
transform: translateX(5px);
clip-path: inset(70% 0 5% 0);
}
}@import "tailwindcss";
@theme {
--color-thv02-bg: #080810;
--color-thv02-bg: oklch(0.13 0.02 285);
--color-thv02-text: #f0f0f0;
--color-thv02-text: oklch(0.95 0 0);
--color-thv02-dim: #444444;
--color-thv02-dim: oklch(0.4 0 0);
--color-thv02-red: #ff003c;
--color-thv02-red: oklch(0.62 0.28 17);
--color-thv02-blue: #00e5ff;
--color-thv02-blue: oklch(0.83 0.15 203);
--color-thv02-green: #39ff14;
--color-thv02-green: oklch(0.88 0.29 135);
--color-thv02-yellow: #ffe600;
--color-thv02-yellow: oklch(0.91 0.2 103);
--font-thv02-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-thv02-rgb-b: thv02-rgb-b .4s steps(2) infinite;
--animate-thv02-rgb-a: thv02-rgb-a .4s steps(2) .1s infinite;
--animate-thv02-scan: thv02-scan .8s linear infinite;
--animate-thv02-slice-t: thv02-slice-t .5s steps(4) infinite;
--animate-thv02-slice-b: thv02-slice-b .5s steps(4) .15s infinite;
--animate-thv02-vhs-r: thv02-vhs-r .6s steps(3) infinite;
--animate-thv02-vhs-b: thv02-vhs-b .6s steps(3) .2s infinite;
}
.thv-02 {
width: 100%;
min-height: 100svh;
}
.thv-02 ::selection {
background: var(--color-thv02-red);
color: #fff;
}
/* Motion definitions stay in CSS; @theme above mints the animate-* utilities. */
@keyframes thv02-rgb-b {
0% {
clip-path: inset(0 0 95% 0);
transform: translateX(-3px);
}
20% {
clip-path: inset(30% 0 50% 0);
transform: translateX(3px);
}
40% {
clip-path: inset(60% 0 20% 0);
transform: translateX(-2px);
}
60% {
clip-path: inset(10% 0 75% 0);
transform: translateX(4px);
}
80% {
clip-path: inset(80% 0 5% 0);
transform: translateX(-3px);
}
100% {
clip-path: inset(45% 0 40% 0);
transform: translateX(2px);
}
}
@keyframes thv02-rgb-a {
0% {
clip-path: inset(50% 0 30% 0);
transform: translateX(3px);
}
25% {
clip-path: inset(5% 0 80% 0);
transform: translateX(-4px);
}
50% {
clip-path: inset(70% 0 15% 0);
transform: translateX(2px);
}
75% {
clip-path: inset(20% 0 60% 0);
transform: translateX(-3px);
}
100% {
clip-path: inset(40% 0 45% 0);
transform: translateX(3px);
}
}
@keyframes thv02-scan {
0% {
background-position-y: 0;
}
100% {
background-position-y: 100px;
}
}
@keyframes thv02-slice-t {
0% {
clip-path: inset(0 0 85% 0);
transform: translateX(6px) skewX(-2deg);
}
33% {
clip-path: inset(25% 0 55% 0);
transform: translateX(-5px);
}
66% {
clip-path: inset(60% 0 10% 0);
transform: translateX(4px) skewX(1deg);
}
100% {
clip-path: inset(10% 0 75% 0);
transform: translateX(-6px);
}
}
@keyframes thv02-slice-b {
0% {
clip-path: inset(80% 0 0 0);
transform: translateX(-4px);
}
33% {
clip-path: inset(50% 0 20% 0);
transform: translateX(6px);
}
66% {
clip-path: inset(15% 0 70% 0);
transform: translateX(-3px) skewX(2deg);
}
100% {
clip-path: inset(65% 0 5% 0);
transform: translateX(5px);
}
}
@keyframes thv02-vhs-r {
0% {
transform: translateX(-6px);
clip-path: inset(20% 0 60% 0);
}
50% {
transform: translateX(4px);
clip-path: inset(55% 0 10% 0);
}
100% {
transform: translateX(-5px);
clip-path: inset(5% 0 80% 0);
}
}
@keyframes thv02-vhs-b {
0% {
transform: translateX(6px);
clip-path: inset(40% 0 30% 0);
}
50% {
transform: translateX(-4px);
clip-path: inset(10% 0 65% 0);
}
100% {
transform: translateX(5px);
clip-path: inset(70% 0 5% 0);
}
}<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;500;600;700&family=JetBrains+Mono:wght@400;500;600;800&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;500;600;700&family=JetBrains+Mono:wght@400;500;600;800&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The port's dividing line is clean:
@keyframesin CSS, everything that selects or positions in the class list.@theme's--animate-*entries are the bridge. - UI change from the CSS original: each glitch word is now
tabindex="0"with afocus-visible:twin and a visible ring, so the effect exists for keyboard users; the word itself stays real text, so the accessibility tree is unchanged. motion-reduce:before:hiddenis deliberately blunt. For a steps() glitch, suppressing the layers entirely is the honest reduced-motion answer — a slower glitch is still a glitch.
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 Text Glitch Hover Effect
Source: https://codefronts.com/motion/tailwind-css-hover-effects/tailwind-text-glitch-hover-effect/
Four glitch hovers — RGB channel split, scanline flicker, noise slice and VHS chromatic aberration. Keyframes stay in the stylesheet (they are motion definitions, not design-scale values), but each becomes an --animate-* token, so firing one is a single hover:before:animate-* utility.
## HTML
```html
<section class="thv-02 w-full min-h-svh grid place-items-center bg-thv02-bg font-thv02-sans px-6 py-14">
<div class="w-[min(820px,100%)] grid grid-cols-[repeat(auto-fit,minmax(260px,1fr))] gap-10">
<div class="flex flex-col items-center gap-6 px-8 py-14 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<span tabindex="0" data-text="GLITCH" class="group relative inline-block text-[clamp(2rem,6vw,3.5rem)] font-black tracking-[0.08em] text-thv02-text rounded-xs focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-thv02-red before:content-[attr(data-text)] before:absolute before:inset-0 before:left-0.5 before:text-thv02-red before:pointer-events-none after:content-[attr(data-text)] after:absolute after:inset-0 after:-left-0.5 after:text-thv02-blue after:pointer-events-none hover:before:animate-thv02-rgb-b hover:after:animate-thv02-rgb-a focus-visible:before:animate-thv02-rgb-b focus-visible:after:animate-thv02-rgb-a motion-reduce:before:hidden motion-reduce:after:hidden">GLITCH</span>
<span class="text-[11px] tracking-[0.15em] uppercase text-thv02-dim">RGB Channel Split</span>
</div>
<div class="flex flex-col items-center gap-6 px-8 py-14 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<span tabindex="0" data-text="SCANLINE" class="relative inline-block text-[clamp(1.6rem,5vw,2.6rem)] font-black tracking-[0.08em] text-thv02-green rounded-xs focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-thv02-green before:content-[''] before:absolute before:inset-0 before:opacity-0 before:mix-blend-multiply before:transition-opacity before:duration-200 before:[background:repeating-linear-gradient(0deg,transparent,transparent_2px,rgba(0,0,0,.4)_2px,rgba(0,0,0,.4)_4px)] before:[background-size:100%_4px] after:content-[attr(data-text)] after:absolute after:inset-0 after:left-0.5 after:text-thv02-green after:opacity-0 after:transition-opacity after:duration-200 hover:before:opacity-100 hover:before:animate-thv02-scan hover:after:opacity-70 hover:after:animate-thv02-rgb-b focus-visible:before:opacity-100 focus-visible:after:opacity-70 motion-reduce:before:hidden motion-reduce:after:hidden">SCANLINE</span>
<span class="text-[11px] tracking-[0.15em] uppercase text-thv02-dim">Scanline Flicker</span>
</div>
<div class="flex flex-col items-center gap-6 px-8 py-14 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<span tabindex="0" data-text="NOISE" class="relative inline-block text-[clamp(2rem,6vw,3.5rem)] font-black tracking-[0.08em] text-thv02-yellow rounded-xs focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-thv02-yellow before:content-[attr(data-text)] before:absolute before:inset-0 before:text-thv02-red before:[clip-path:inset(0_0_100%_0)] after:content-[attr(data-text)] after:absolute after:inset-0 after:text-thv02-blue after:[clip-path:inset(100%_0_0_0)] hover:before:animate-thv02-slice-t hover:after:animate-thv02-slice-b focus-visible:before:animate-thv02-slice-t focus-visible:after:animate-thv02-slice-b motion-reduce:before:hidden motion-reduce:after:hidden">NOISE</span>
<span class="text-[11px] tracking-[0.15em] uppercase text-thv02-dim">Noise Slice</span>
</div>
<div class="flex flex-col items-center gap-6 px-8 py-14 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<span tabindex="0" data-text="VHS" class="relative inline-block text-[clamp(2rem,6vw,3.5rem)] font-black tracking-[0.08em] text-white rounded-xs focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-thv02-blue before:content-[attr(data-text)] before:absolute before:inset-0 before:text-thv02-red before:mix-blend-screen after:content-[attr(data-text)] after:absolute after:inset-0 after:text-thv02-blue after:mix-blend-screen hover:before:animate-thv02-vhs-r hover:after:animate-thv02-vhs-b focus-visible:before:animate-thv02-vhs-r focus-visible:after:animate-thv02-vhs-b motion-reduce:before:hidden motion-reduce:after:hidden">VHS</span>
<span class="text-[11px] tracking-[0.15em] uppercase text-thv02-dim">VHS Chromatic</span>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-thv02-bg: #080810;
--color-thv02-bg: oklch(0.13 0.02 285);
--color-thv02-text: #f0f0f0;
--color-thv02-text: oklch(0.95 0 0);
--color-thv02-dim: #444444;
--color-thv02-dim: oklch(0.4 0 0);
--color-thv02-red: #ff003c;
--color-thv02-red: oklch(0.62 0.28 17);
--color-thv02-blue: #00e5ff;
--color-thv02-blue: oklch(0.83 0.15 203);
--color-thv02-green: #39ff14;
--color-thv02-green: oklch(0.88 0.29 135);
--color-thv02-yellow: #ffe600;
--color-thv02-yellow: oklch(0.91 0.2 103);
--font-thv02-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-thv02-rgb-b: thv02-rgb-b .4s steps(2) infinite;
--animate-thv02-rgb-a: thv02-rgb-a .4s steps(2) .1s infinite;
--animate-thv02-scan: thv02-scan .8s linear infinite;
--animate-thv02-slice-t: thv02-slice-t .5s steps(4) infinite;
--animate-thv02-slice-b: thv02-slice-b .5s steps(4) .15s infinite;
--animate-thv02-vhs-r: thv02-vhs-r .6s steps(3) infinite;
--animate-thv02-vhs-b: thv02-vhs-b .6s steps(3) .2s infinite;
}
.thv-02 {
width: 100%;
min-height: 100svh;
}
.thv-02 ::selection {
background: var(--color-thv02-red);
color: #fff;
}
/* Motion definitions stay in CSS; @theme above mints the animate-* utilities. */
@keyframes thv02-rgb-b {
0% {
clip-path: inset(0 0 95% 0);
transform: translateX(-3px);
}
20% {
clip-path: inset(30% 0 50% 0);
transform: translateX(3px);
}
40% {
clip-path: inset(60% 0 20% 0);
transform: translateX(-2px);
}
60% {
clip-path: inset(10% 0 75% 0);
transform: translateX(4px);
}
80% {
clip-path: inset(80% 0 5% 0);
transform: translateX(-3px);
}
100% {
clip-path: inset(45% 0 40% 0);
transform: translateX(2px);
}
}
@keyframes thv02-rgb-a {
0% {
clip-path: inset(50% 0 30% 0);
transform: translateX(3px);
}
25% {
clip-path: inset(5% 0 80% 0);
transform: translateX(-4px);
}
50% {
clip-path: inset(70% 0 15% 0);
transform: translateX(2px);
}
75% {
clip-path: inset(20% 0 60% 0);
transform: translateX(-3px);
}
100% {
clip-path: inset(40% 0 45% 0);
transform: translateX(3px);
}
}
@keyframes thv02-scan {
0% {
background-position-y: 0;
}
100% {
background-position-y: 100px;
}
}
@keyframes thv02-slice-t {
0% {
clip-path: inset(0 0 85% 0);
transform: translateX(6px) skewX(-2deg);
}
33% {
clip-path: inset(25% 0 55% 0);
transform: translateX(-5px);
}
66% {
clip-path: inset(60% 0 10% 0);
transform: translateX(4px) skewX(1deg);
}
100% {
clip-path: inset(10% 0 75% 0);
transform: translateX(-6px);
}
}
@keyframes thv02-slice-b {
0% {
clip-path: inset(80% 0 0 0);
transform: translateX(-4px);
}
33% {
clip-path: inset(50% 0 20% 0);
transform: translateX(6px);
}
66% {
clip-path: inset(15% 0 70% 0);
transform: translateX(-3px) skewX(2deg);
}
100% {
clip-path: inset(65% 0 5% 0);
transform: translateX(5px);
}
}
@keyframes thv02-vhs-r {
0% {
transform: translateX(-6px);
clip-path: inset(20% 0 60% 0);
}
50% {
transform: translateX(4px);
clip-path: inset(55% 0 10% 0);
}
100% {
transform: translateX(-5px);
clip-path: inset(5% 0 80% 0);
}
}
@keyframes thv02-vhs-b {
0% {
transform: translateX(6px);
clip-path: inset(40% 0 30% 0);
}
50% {
transform: translateX(-4px);
clip-path: inset(10% 0 65% 0);
}
100% {
transform: translateX(5px);
clip-path: inset(70% 0 5% 0);
}
}
```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 Text Glitch Hover Effect
Source: https://codefronts.com/motion/tailwind-css-hover-effects/tailwind-text-glitch-hover-effect/
Four glitch hovers — RGB channel split, scanline flicker, noise slice and VHS chromatic aberration. Keyframes stay in the stylesheet (they are motion definitions, not design-scale values), but each becomes an --animate-* token, so firing one is a single hover:before:animate-* utility.
## HTML
```html
<section class="thv-02 w-full min-h-svh grid place-items-center bg-thv02-bg font-thv02-sans px-6 py-14">
<div class="w-[min(820px,100%)] grid grid-cols-[repeat(auto-fit,minmax(260px,1fr))] gap-10">
<div class="flex flex-col items-center gap-6 px-8 py-14 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<span tabindex="0" data-text="GLITCH" class="group relative inline-block text-[clamp(2rem,6vw,3.5rem)] font-black tracking-[0.08em] text-thv02-text rounded-xs focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-thv02-red before:content-[attr(data-text)] before:absolute before:inset-0 before:left-0.5 before:text-thv02-red before:pointer-events-none after:content-[attr(data-text)] after:absolute after:inset-0 after:-left-0.5 after:text-thv02-blue after:pointer-events-none hover:before:animate-thv02-rgb-b hover:after:animate-thv02-rgb-a focus-visible:before:animate-thv02-rgb-b focus-visible:after:animate-thv02-rgb-a motion-reduce:before:hidden motion-reduce:after:hidden">GLITCH</span>
<span class="text-[11px] tracking-[0.15em] uppercase text-thv02-dim">RGB Channel Split</span>
</div>
<div class="flex flex-col items-center gap-6 px-8 py-14 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<span tabindex="0" data-text="SCANLINE" class="relative inline-block text-[clamp(1.6rem,5vw,2.6rem)] font-black tracking-[0.08em] text-thv02-green rounded-xs focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-thv02-green before:content-[''] before:absolute before:inset-0 before:opacity-0 before:mix-blend-multiply before:transition-opacity before:duration-200 before:[background:repeating-linear-gradient(0deg,transparent,transparent_2px,rgba(0,0,0,.4)_2px,rgba(0,0,0,.4)_4px)] before:[background-size:100%_4px] after:content-[attr(data-text)] after:absolute after:inset-0 after:left-0.5 after:text-thv02-green after:opacity-0 after:transition-opacity after:duration-200 hover:before:opacity-100 hover:before:animate-thv02-scan hover:after:opacity-70 hover:after:animate-thv02-rgb-b focus-visible:before:opacity-100 focus-visible:after:opacity-70 motion-reduce:before:hidden motion-reduce:after:hidden">SCANLINE</span>
<span class="text-[11px] tracking-[0.15em] uppercase text-thv02-dim">Scanline Flicker</span>
</div>
<div class="flex flex-col items-center gap-6 px-8 py-14 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<span tabindex="0" data-text="NOISE" class="relative inline-block text-[clamp(2rem,6vw,3.5rem)] font-black tracking-[0.08em] text-thv02-yellow rounded-xs focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-thv02-yellow before:content-[attr(data-text)] before:absolute before:inset-0 before:text-thv02-red before:[clip-path:inset(0_0_100%_0)] after:content-[attr(data-text)] after:absolute after:inset-0 after:text-thv02-blue after:[clip-path:inset(100%_0_0_0)] hover:before:animate-thv02-slice-t hover:after:animate-thv02-slice-b focus-visible:before:animate-thv02-slice-t focus-visible:after:animate-thv02-slice-b motion-reduce:before:hidden motion-reduce:after:hidden">NOISE</span>
<span class="text-[11px] tracking-[0.15em] uppercase text-thv02-dim">Noise Slice</span>
</div>
<div class="flex flex-col items-center gap-6 px-8 py-14 rounded-xl bg-white/[0.025] border border-white/[0.07]">
<span tabindex="0" data-text="VHS" class="relative inline-block text-[clamp(2rem,6vw,3.5rem)] font-black tracking-[0.08em] text-white rounded-xs focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-thv02-blue before:content-[attr(data-text)] before:absolute before:inset-0 before:text-thv02-red before:mix-blend-screen after:content-[attr(data-text)] after:absolute after:inset-0 after:text-thv02-blue after:mix-blend-screen hover:before:animate-thv02-vhs-r hover:after:animate-thv02-vhs-b focus-visible:before:animate-thv02-vhs-r focus-visible:after:animate-thv02-vhs-b motion-reduce:before:hidden motion-reduce:after:hidden">VHS</span>
<span class="text-[11px] tracking-[0.15em] uppercase text-thv02-dim">VHS Chromatic</span>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-thv02-bg: #080810;
--color-thv02-bg: oklch(0.13 0.02 285);
--color-thv02-text: #f0f0f0;
--color-thv02-text: oklch(0.95 0 0);
--color-thv02-dim: #444444;
--color-thv02-dim: oklch(0.4 0 0);
--color-thv02-red: #ff003c;
--color-thv02-red: oklch(0.62 0.28 17);
--color-thv02-blue: #00e5ff;
--color-thv02-blue: oklch(0.83 0.15 203);
--color-thv02-green: #39ff14;
--color-thv02-green: oklch(0.88 0.29 135);
--color-thv02-yellow: #ffe600;
--color-thv02-yellow: oklch(0.91 0.2 103);
--font-thv02-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-thv02-rgb-b: thv02-rgb-b .4s steps(2) infinite;
--animate-thv02-rgb-a: thv02-rgb-a .4s steps(2) .1s infinite;
--animate-thv02-scan: thv02-scan .8s linear infinite;
--animate-thv02-slice-t: thv02-slice-t .5s steps(4) infinite;
--animate-thv02-slice-b: thv02-slice-b .5s steps(4) .15s infinite;
--animate-thv02-vhs-r: thv02-vhs-r .6s steps(3) infinite;
--animate-thv02-vhs-b: thv02-vhs-b .6s steps(3) .2s infinite;
}
.thv-02 {
width: 100%;
min-height: 100svh;
}
.thv-02 ::selection {
background: var(--color-thv02-red);
color: #fff;
}
/* Motion definitions stay in CSS; @theme above mints the animate-* utilities. */
@keyframes thv02-rgb-b {
0% {
clip-path: inset(0 0 95% 0);
transform: translateX(-3px);
}
20% {
clip-path: inset(30% 0 50% 0);
transform: translateX(3px);
}
40% {
clip-path: inset(60% 0 20% 0);
transform: translateX(-2px);
}
60% {
clip-path: inset(10% 0 75% 0);
transform: translateX(4px);
}
80% {
clip-path: inset(80% 0 5% 0);
transform: translateX(-3px);
}
100% {
clip-path: inset(45% 0 40% 0);
transform: translateX(2px);
}
}
@keyframes thv02-rgb-a {
0% {
clip-path: inset(50% 0 30% 0);
transform: translateX(3px);
}
25% {
clip-path: inset(5% 0 80% 0);
transform: translateX(-4px);
}
50% {
clip-path: inset(70% 0 15% 0);
transform: translateX(2px);
}
75% {
clip-path: inset(20% 0 60% 0);
transform: translateX(-3px);
}
100% {
clip-path: inset(40% 0 45% 0);
transform: translateX(3px);
}
}
@keyframes thv02-scan {
0% {
background-position-y: 0;
}
100% {
background-position-y: 100px;
}
}
@keyframes thv02-slice-t {
0% {
clip-path: inset(0 0 85% 0);
transform: translateX(6px) skewX(-2deg);
}
33% {
clip-path: inset(25% 0 55% 0);
transform: translateX(-5px);
}
66% {
clip-path: inset(60% 0 10% 0);
transform: translateX(4px) skewX(1deg);
}
100% {
clip-path: inset(10% 0 75% 0);
transform: translateX(-6px);
}
}
@keyframes thv02-slice-b {
0% {
clip-path: inset(80% 0 0 0);
transform: translateX(-4px);
}
33% {
clip-path: inset(50% 0 20% 0);
transform: translateX(6px);
}
66% {
clip-path: inset(15% 0 70% 0);
transform: translateX(-3px) skewX(2deg);
}
100% {
clip-path: inset(65% 0 5% 0);
transform: translateX(5px);
}
}
@keyframes thv02-vhs-r {
0% {
transform: translateX(-6px);
clip-path: inset(20% 0 60% 0);
}
50% {
transform: translateX(4px);
clip-path: inset(55% 0 10% 0);
}
100% {
transform: translateX(-5px);
clip-path: inset(5% 0 80% 0);
}
}
@keyframes thv02-vhs-b {
0% {
transform: translateX(6px);
clip-path: inset(40% 0 30% 0);
}
50% {
transform: translateX(-4px);
clip-path: inset(10% 0 65% 0);
}
100% {
transform: translateX(5px);
clip-path: inset(70% 0 5% 0);
}
}
```How this works
Keyframes cannot be utilities; firing them can. The six @keyframes blocks stay in the stylesheet — a clip-path sequence is authored motion, not a token. But each is registered in @theme as --animate-thv02-*, which mints animate-thv02-rgb-b, and the trigger reads hover:before:animate-thv02-rgb-b. The resting offsets stay inline: before:text-thv02-red before:left-0.5.
The rest is arbitrary properties, correctly. before:[clip-path:inset(0_0_100%_0)] and the scanline's before:[background:repeating-linear-gradient(...)] have no utility form and should not have one. The underscore escape carries the multi-value syntax through intact.
Blend mode is the exception. mix-blend-screen and mix-blend-multiply are real utilities, so the VHS and scanline layers read cleanly.
Make it yours
- Intensity lives in the stylesheet keyframes; offsets live in the markup — widen
before:left-0.5tobefore:left-1for a harder resting split. - Speed is one
@themeedit:--animate-thv02-rgb-b: thv02-rgb-b .2s steps(2) infinite. - Recolour through
--color-thv02-red/--color-thv02-blue; both pseudo-elements read the tokens.
Gotchas — read before shipping
before:content-[attr(data-text)]renders nothing if thedata-textattribute is missing — the glitch silently disappears.mix-blend-screenneeds a dark ground; on a light card the channels wash to white.- Under
motion-reduce:the pseudo-elements are hidden rather than slowed — a steps() glitch at any speed is exactly the motion the setting exists to suppress.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
clip-path: inset() and mix-blend-mode are Baseline; the floor is Tailwind v4's generated sheet.