20 / 20
All 20 in 20 Tailwind CSS Grain Texture
- 01Tailwind SVG feTurbulence Noise Parameter Breakdown
- 02Tailwind baseFrequency and numOctaves Grain Size Control
- 03Tailwind Data URI SVG Noise Texture Without an Image Request
- 04Tailwind Grain Opacity and mix-blend-mode Composite Matrix
- 05Tailwind Tinted SVG Noise with feColorMatrix
- 06Tailwind Grain Paint Cost and Byte Size Measured Live
- 07Tailwind Anti-Banding Grainy CSS Gradient Hero
- 08Tailwind Vignette Photo Overlay with Film Grain
- 09Tailwind Tactile Card Surface with Subtle Noise
- 10Tailwind Full-Bleed Hero Section Noise Background
- 11Tailwind Noise Layer Order Over backdrop-filter Glass
- 12Tailwind Textured Button and Input Surfaces
- 13Tailwind Textured Section Divider with SVG Grain
- 14Tailwind Static Halftone Dot Print Effect
- 15Tailwind Dithered Bayer Matrix Shading Effect
- 16Tailwind Paper Stock and Letterpress Press Effect
- 17Tailwind Risograph Print Misregistration Texture
- 18Tailwind Woven Fabric and Canvas Texture Surface
- 19Tailwind Dark Mode Noise Without Muddy Shadows
- 20Tailwind Reduced Motion Static Grain Fallback
Tailwind Reduced Motion Static Grain Fallback
Animated grain is the single most likely thing in a texture collection to make somebody feel ill — rapid luminance change across a large area is exactly the trigger for vestibular disorders, migraine and photosensitivity. This demo animates a film gate properly (compositor transforms, steps(8)), and then designs the reduced-motion version instead of switching the animation off.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="gtw-20 relative block w-full min-h-svh bg-gtw20-bg text-gtw20-ink" aria-labelledby="gtw-20-title">
<div class="absolute top-[clamp(1rem,3vw,1.75rem)] right-[clamp(0.85rem,3vw,1.75rem)] z-60">
<input type="checkbox" id="gtw-20-theme" class="gtw-20-theme peer sr-only">
<label for="gtw-20-theme" class="grid place-items-center w-11 h-11 rounded-full border border-white/30 bg-[oklch(0.14_0.004_85/0.5)] backdrop-blur-[8px] text-white cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-gtw20-acc peer-focus-visible:outline-offset-[3px]">
<svg viewBox="0 0 24 24" class="w-5 h-5" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="sr-only">Toggle dark theme</span>
</label>
</div>
<div class="relative isolate overflow-hidden grid items-end min-h-[min(82svh,42rem)] bg-[#0b0b0c]">
<img src="https://picsum.photos/id/1016/2000/1200" alt="" aria-hidden="true" width="2000" height="1200" class="absolute inset-0 z-0 w-full h-full object-cover [filter:grayscale(0.55)_contrast(1.12)_brightness(0.86)_sepia(0.12)] motion-reduce:[filter:grayscale(0.7)_contrast(1.18)_brightness(0.82)_sepia(0.16)]">
<span aria-hidden="true" class="absolute inset-0 z-[1] pointer-events-none opacity-70 motion-reduce:opacity-45 bg-[repeating-linear-gradient(0deg,rgba(255,255,255,0.035)_0_1px,transparent_1px_3px)]"></span>
<span aria-hidden="true" class="absolute -inset-[25%] z-[2] pointer-events-none bg-[image:var(--gtw20-noise)] opacity-[var(--gtw20-anim)] mix-blend-overlay animate-gtw20-gate will-change-transform motion-reduce:animate-none motion-reduce:inset-0 motion-reduce:opacity-[var(--gtw20-static)] motion-reduce:mix-blend-soft-light motion-reduce:will-change-auto"></span>
<span aria-hidden="true" class="absolute inset-0 z-[3] pointer-events-none bg-[radial-gradient(122%_100%_at_50%_44%,transparent_42%,oklch(0.12_0.003_85/0.35)_78%,oklch(0.12_0.003_85/0.66)_100%)] motion-reduce:bg-[radial-gradient(118%_100%_at_50%_42%,transparent_34%,oklch(0.12_0.003_85/0.44)_74%,oklch(0.12_0.003_85/0.78)_100%)]"></span>
<div class="relative z-[5] w-[min(72rem,calc(100%-clamp(1.5rem,5vw,4rem)))] mx-auto grid gap-[0.8rem] max-w-[42rem] py-[clamp(2.5rem,8vw,4.5rem)] text-white">
<p class="m-0 justify-self-start px-3 py-[0.35rem] rounded-full border border-white/30 bg-[oklch(0.12_0.003_85/0.5)] backdrop-blur-[5px] font-gtw20-mono text-[0.72rem] font-medium leading-[1.4] tracking-[0.1em] uppercase">
<span class="inline motion-reduce:hidden">Projector running · animated grain</span>
<span class="hidden motion-reduce:inline">Reduced motion · static frame held</span>
</p>
<h1 id="gtw-20-title" class="m-0 font-gtw20-display text-[clamp(2.2rem,7vw,4.4rem)] font-semibold leading-[0.98] tracking-[-0.04em] text-balance">Reel four, gate check</h1>
<p class="m-0 max-w-[34rem] text-white text-[clamp(0.96rem,2vw,1.11rem)] text-pretty">The grain layer translates on the compositor in eight discrete steps. If your system asks for reduced motion, it stops — and a heavier, deliberately composed static treatment takes over.</p>
</div>
</div>
<main class="w-[min(72rem,calc(100%-clamp(1.5rem,5vw,4rem)))] mx-auto py-[clamp(2.5rem,7vw,4rem)] grid gap-[clamp(1.6rem,4vw,2.4rem)]">
<section class="grid gap-4" aria-labelledby="gtw-20-h-rules">
<h2 id="gtw-20-h-rules" class="m-0 pb-[0.7rem] border-b border-gtw20-line font-gtw20-display text-[clamp(1.2rem,3vw,1.6rem)] font-semibold leading-tight tracking-[-0.025em]">Four rules for animated texture</h2>
<ul class="list-none m-0 p-0 grid gap-[clamp(0.7rem,1.8vw,1.1rem)] grid-cols-[repeat(auto-fit,minmax(min(100%,15rem),1fr))]">
<li class="relative isolate overflow-hidden grid content-start gap-[0.3rem] min-w-0 p-[clamp(1rem,2.4vw,1.4rem)] rounded-[14px] border border-gtw20-line bg-gtw20-surf after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw20-noise)] after:opacity-[0.05] after:mix-blend-overlay">
<span class="font-gtw20-mono text-[0.7rem] font-medium leading-none tracking-[0.14em] text-gtw20-acc">01</span>
<b class="font-gtw20-display text-[0.98rem] font-semibold leading-[1.25] tracking-[-0.01em] text-balance">Translate, never background-position</b>
<span class="text-gtw20-mut text-[0.85rem] text-pretty">One is a compositor transform. The other repaints a viewport-sized background every frame.</span>
</li>
<li class="relative isolate overflow-hidden grid content-start gap-[0.3rem] min-w-0 p-[clamp(1rem,2.4vw,1.4rem)] rounded-[14px] border border-gtw20-line bg-gtw20-surf after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw20-noise)] after:opacity-[0.05] after:mix-blend-overlay">
<span class="font-gtw20-mono text-[0.7rem] font-medium leading-none tracking-[0.14em] text-gtw20-acc">02</span>
<b class="font-gtw20-display text-[0.98rem] font-semibold leading-[1.25] tracking-[-0.01em] text-balance">steps(), never linear</b>
<span class="text-gtw20-mut text-[0.85rem] text-pretty">Film grain jumps between frames. A smooth slide reads as sandpaper being dragged.</span>
</li>
<li class="relative isolate overflow-hidden grid content-start gap-[0.3rem] min-w-0 p-[clamp(1rem,2.4vw,1.4rem)] rounded-[14px] border border-gtw20-line bg-gtw20-surf after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw20-noise)] after:opacity-[0.05] after:mix-blend-overlay">
<span class="font-gtw20-mono text-[0.7rem] font-medium leading-none tracking-[0.14em] text-gtw20-acc">03</span>
<b class="font-gtw20-display text-[0.98rem] font-semibold leading-[1.25] tracking-[-0.01em] text-balance">Never animate filter: url()</b>
<span class="text-gtw20-mut text-[0.85rem] text-pretty">The turbulence re-evaluates every frame. Animate a static texture layer instead.</span>
</li>
<li class="relative isolate overflow-hidden grid content-start gap-[0.3rem] min-w-0 p-[clamp(1rem,2.4vw,1.4rem)] rounded-[14px] border border-gtw20-line bg-gtw20-surf after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw20-noise)] after:opacity-[0.05] after:mix-blend-overlay">
<span class="font-gtw20-mono text-[0.7rem] font-medium leading-none tracking-[0.14em] text-gtw20-acc">04</span>
<b class="font-gtw20-display text-[0.98rem] font-semibold leading-[1.25] tracking-[-0.01em] text-balance">Design the fallback</b>
<span class="text-gtw20-mut text-[0.85rem] text-pretty">Not <code class="font-gtw20-mono text-[0.9em]">animation: none</code>. Heavier grain, a vignette, one composed frame — a real design.</span>
</li>
</ul>
</section>
<aside class="grid gap-2 p-[clamp(1.2rem,3vw,1.8rem)] rounded-[14px] border border-gtw20-line bg-[color-mix(in_oklab,var(--color-gtw20-acc)_8%,transparent)]">
<h2 class="m-0 font-gtw20-display text-[1.05rem] font-semibold leading-tight">Why flicker is the specific risk</h2>
<p class="m-0 max-w-[52rem] text-gtw20-mut text-[0.92rem] text-pretty">Vestibular disorders, migraine and photosensitivity respond to rapid luminance change over large areas more reliably than to movement. Animated grain is precisely that. If it flickers across a large surface, honour <code class="font-gtw20-mono text-[0.9em]">prefers-reduced-motion</code> without exception — and give those users a treatment you would be happy to ship as the only one.</p>
</aside>
</main>
</section><section class="gtw-20 relative block w-full min-h-svh bg-gtw20-bg text-gtw20-ink" aria-labelledby="gtw-20-title">
<div class="absolute top-[clamp(1rem,3vw,1.75rem)] right-[clamp(0.85rem,3vw,1.75rem)] z-60">
<input type="checkbox" id="gtw-20-theme" class="gtw-20-theme peer sr-only">
<label for="gtw-20-theme" class="grid place-items-center w-11 h-11 rounded-full border border-white/30 bg-[oklch(0.14_0.004_85/0.5)] backdrop-blur-[8px] text-white cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-gtw20-acc peer-focus-visible:outline-offset-[3px]">
<svg viewBox="0 0 24 24" class="w-5 h-5" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="sr-only">Toggle dark theme</span>
</label>
</div>
<div class="relative isolate overflow-hidden grid items-end min-h-[min(82svh,42rem)] bg-[#0b0b0c]">
<img src="https://picsum.photos/id/1016/2000/1200" alt="" aria-hidden="true" width="2000" height="1200" class="absolute inset-0 z-0 w-full h-full object-cover [filter:grayscale(0.55)_contrast(1.12)_brightness(0.86)_sepia(0.12)] motion-reduce:[filter:grayscale(0.7)_contrast(1.18)_brightness(0.82)_sepia(0.16)]">
<span aria-hidden="true" class="absolute inset-0 z-[1] pointer-events-none opacity-70 motion-reduce:opacity-45 bg-[repeating-linear-gradient(0deg,rgba(255,255,255,0.035)_0_1px,transparent_1px_3px)]"></span>
<span aria-hidden="true" class="absolute -inset-[25%] z-[2] pointer-events-none bg-[image:var(--gtw20-noise)] opacity-[var(--gtw20-anim)] mix-blend-overlay animate-gtw20-gate will-change-transform motion-reduce:animate-none motion-reduce:inset-0 motion-reduce:opacity-[var(--gtw20-static)] motion-reduce:mix-blend-soft-light motion-reduce:will-change-auto"></span>
<span aria-hidden="true" class="absolute inset-0 z-[3] pointer-events-none bg-[radial-gradient(122%_100%_at_50%_44%,transparent_42%,oklch(0.12_0.003_85/0.35)_78%,oklch(0.12_0.003_85/0.66)_100%)] motion-reduce:bg-[radial-gradient(118%_100%_at_50%_42%,transparent_34%,oklch(0.12_0.003_85/0.44)_74%,oklch(0.12_0.003_85/0.78)_100%)]"></span>
<div class="relative z-[5] w-[min(72rem,calc(100%-clamp(1.5rem,5vw,4rem)))] mx-auto grid gap-[0.8rem] max-w-[42rem] py-[clamp(2.5rem,8vw,4.5rem)] text-white">
<p class="m-0 justify-self-start px-3 py-[0.35rem] rounded-full border border-white/30 bg-[oklch(0.12_0.003_85/0.5)] backdrop-blur-[5px] font-gtw20-mono text-[0.72rem] font-medium leading-[1.4] tracking-[0.1em] uppercase">
<span class="inline motion-reduce:hidden">Projector running · animated grain</span>
<span class="hidden motion-reduce:inline">Reduced motion · static frame held</span>
</p>
<h1 id="gtw-20-title" class="m-0 font-gtw20-display text-[clamp(2.2rem,7vw,4.4rem)] font-semibold leading-[0.98] tracking-[-0.04em] text-balance">Reel four, gate check</h1>
<p class="m-0 max-w-[34rem] text-white text-[clamp(0.96rem,2vw,1.11rem)] text-pretty">The grain layer translates on the compositor in eight discrete steps. If your system asks for reduced motion, it stops — and a heavier, deliberately composed static treatment takes over.</p>
</div>
</div>
<main class="w-[min(72rem,calc(100%-clamp(1.5rem,5vw,4rem)))] mx-auto py-[clamp(2.5rem,7vw,4rem)] grid gap-[clamp(1.6rem,4vw,2.4rem)]">
<section class="grid gap-4" aria-labelledby="gtw-20-h-rules">
<h2 id="gtw-20-h-rules" class="m-0 pb-[0.7rem] border-b border-gtw20-line font-gtw20-display text-[clamp(1.2rem,3vw,1.6rem)] font-semibold leading-tight tracking-[-0.025em]">Four rules for animated texture</h2>
<ul class="list-none m-0 p-0 grid gap-[clamp(0.7rem,1.8vw,1.1rem)] grid-cols-[repeat(auto-fit,minmax(min(100%,15rem),1fr))]">
<li class="relative isolate overflow-hidden grid content-start gap-[0.3rem] min-w-0 p-[clamp(1rem,2.4vw,1.4rem)] rounded-[14px] border border-gtw20-line bg-gtw20-surf after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw20-noise)] after:opacity-[0.05] after:mix-blend-overlay">
<span class="font-gtw20-mono text-[0.7rem] font-medium leading-none tracking-[0.14em] text-gtw20-acc">01</span>
<b class="font-gtw20-display text-[0.98rem] font-semibold leading-[1.25] tracking-[-0.01em] text-balance">Translate, never background-position</b>
<span class="text-gtw20-mut text-[0.85rem] text-pretty">One is a compositor transform. The other repaints a viewport-sized background every frame.</span>
</li>
<li class="relative isolate overflow-hidden grid content-start gap-[0.3rem] min-w-0 p-[clamp(1rem,2.4vw,1.4rem)] rounded-[14px] border border-gtw20-line bg-gtw20-surf after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw20-noise)] after:opacity-[0.05] after:mix-blend-overlay">
<span class="font-gtw20-mono text-[0.7rem] font-medium leading-none tracking-[0.14em] text-gtw20-acc">02</span>
<b class="font-gtw20-display text-[0.98rem] font-semibold leading-[1.25] tracking-[-0.01em] text-balance">steps(), never linear</b>
<span class="text-gtw20-mut text-[0.85rem] text-pretty">Film grain jumps between frames. A smooth slide reads as sandpaper being dragged.</span>
</li>
<li class="relative isolate overflow-hidden grid content-start gap-[0.3rem] min-w-0 p-[clamp(1rem,2.4vw,1.4rem)] rounded-[14px] border border-gtw20-line bg-gtw20-surf after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw20-noise)] after:opacity-[0.05] after:mix-blend-overlay">
<span class="font-gtw20-mono text-[0.7rem] font-medium leading-none tracking-[0.14em] text-gtw20-acc">03</span>
<b class="font-gtw20-display text-[0.98rem] font-semibold leading-[1.25] tracking-[-0.01em] text-balance">Never animate filter: url()</b>
<span class="text-gtw20-mut text-[0.85rem] text-pretty">The turbulence re-evaluates every frame. Animate a static texture layer instead.</span>
</li>
<li class="relative isolate overflow-hidden grid content-start gap-[0.3rem] min-w-0 p-[clamp(1rem,2.4vw,1.4rem)] rounded-[14px] border border-gtw20-line bg-gtw20-surf after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw20-noise)] after:opacity-[0.05] after:mix-blend-overlay">
<span class="font-gtw20-mono text-[0.7rem] font-medium leading-none tracking-[0.14em] text-gtw20-acc">04</span>
<b class="font-gtw20-display text-[0.98rem] font-semibold leading-[1.25] tracking-[-0.01em] text-balance">Design the fallback</b>
<span class="text-gtw20-mut text-[0.85rem] text-pretty">Not <code class="font-gtw20-mono text-[0.9em]">animation: none</code>. Heavier grain, a vignette, one composed frame — a real design.</span>
</li>
</ul>
</section>
<aside class="grid gap-2 p-[clamp(1.2rem,3vw,1.8rem)] rounded-[14px] border border-gtw20-line bg-[color-mix(in_oklab,var(--color-gtw20-acc)_8%,transparent)]">
<h2 class="m-0 font-gtw20-display text-[1.05rem] font-semibold leading-tight">Why flicker is the specific risk</h2>
<p class="m-0 max-w-[52rem] text-gtw20-mut text-[0.92rem] text-pretty">Vestibular disorders, migraine and photosensitivity respond to rapid luminance change over large areas more reliably than to movement. Animated grain is precisely that. If it flickers across a large surface, honour <code class="font-gtw20-mono text-[0.9em]">prefers-reduced-motion</code> without exception — and give those users a treatment you would be happy to ship as the only one.</p>
</aside>
</main>
</section>@import "tailwindcss";
@theme {
--color-gtw20-bg: #0b0b0c;
--color-gtw20-bg: oklch(0.16 0.003 85);
--color-gtw20-surf: #16161a;
--color-gtw20-surf: oklch(0.22 0.005 85);
--color-gtw20-line: #292930;
--color-gtw20-line: oklch(0.31 0.008 85);
--color-gtw20-ink: #f2f1ee;
--color-gtw20-ink: oklch(0.96 0.005 85);
--color-gtw20-mut: #9b988f;
--color-gtw20-mut: oklch(0.69 0.01 85);
--color-gtw20-acc: #e5c07b;
--color-gtw20-acc: oklch(0.83 0.11 88);
--font-gtw20-display: 'Space Grotesk', 'Inter Tight', ui-sans-serif, system-ui, sans-serif;
--font-gtw20-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
/* The gate animation as a token: mints animate-gtw20-gate, keyframes below. */
--animate-gtw20-gate: gtw-20-gate 0.9s steps(8) infinite;
}
@keyframes gtw-20-gate {
0% {
transform: translate3d(0, 0, 0);
}
12% {
transform: translate3d(-3%, 2%, 0);
}
25% {
transform: translate3d(4%, -3%, 0);
}
37% {
transform: translate3d(-5%, -1%, 0);
}
50% {
transform: translate3d(2%, 4%, 0);
}
62% {
transform: translate3d(-2%, -4%, 0);
}
75% {
transform: translate3d(5%, 1%, 0);
}
87% {
transform: translate3d(-4%, 3%, 0);
}
100% {
transform: translate3d(0, 0, 0);
}
}
.gtw-20 {
width: 100%;
min-height: 100svh;
--gtw20-anim: 0.11;
--gtw20-static: 0.17;
--gtw20-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='gtw20n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23gtw20n)'/%3E%3C/svg%3E");
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.55;
}
.gtw-20:has(.gtw-20-theme:checked) {
--color-gtw20-bg: #f3f2ef;
--color-gtw20-bg: oklch(0.96 0.004 85);
--color-gtw20-surf: #ffffff;
--color-gtw20-surf: oklch(1 0 0);
--color-gtw20-line: #dedbd4;
--color-gtw20-line: oklch(0.89 0.008 85);
--color-gtw20-ink: #0b0b0c;
--color-gtw20-ink: oklch(0.16 0.003 85);
--color-gtw20-mut: #615e57;
--color-gtw20-mut: oklch(0.47 0.01 85);
--color-gtw20-acc: #8a6314;
--color-gtw20-acc: oklch(0.5 0.1 78);
}
@media (prefers-color-scheme: light) {
.gtw-20 {
--color-gtw20-bg: #f3f2ef;
--color-gtw20-bg: oklch(0.96 0.004 85);
--color-gtw20-surf: #ffffff;
--color-gtw20-surf: oklch(1 0 0);
--color-gtw20-line: #dedbd4;
--color-gtw20-line: oklch(0.89 0.008 85);
--color-gtw20-ink: #0b0b0c;
--color-gtw20-ink: oklch(0.16 0.003 85);
--color-gtw20-mut: #615e57;
--color-gtw20-mut: oklch(0.47 0.01 85);
--color-gtw20-acc: #8a6314;
--color-gtw20-acc: oklch(0.5 0.1 78);
}
.gtw-20:has(.gtw-20-theme:checked) {
--color-gtw20-bg: #0b0b0c;
--color-gtw20-bg: oklch(0.16 0.003 85);
--color-gtw20-surf: #16161a;
--color-gtw20-surf: oklch(0.22 0.005 85);
--color-gtw20-line: #292930;
--color-gtw20-line: oklch(0.31 0.008 85);
--color-gtw20-ink: #f2f1ee;
--color-gtw20-ink: oklch(0.96 0.005 85);
--color-gtw20-mut: #9b988f;
--color-gtw20-mut: oklch(0.69 0.01 85);
--color-gtw20-acc: #e5c07b;
--color-gtw20-acc: oklch(0.83 0.11 88);
}
}
@media (prefers-color-scheme: dark) {
.gtw-20:has(.gtw-20-theme:checked) {
--color-gtw20-bg: #f3f2ef;
--color-gtw20-bg: oklch(0.96 0.004 85);
--color-gtw20-surf: #ffffff;
--color-gtw20-surf: oklch(1 0 0);
--color-gtw20-line: #dedbd4;
--color-gtw20-line: oklch(0.89 0.008 85);
--color-gtw20-ink: #0b0b0c;
--color-gtw20-ink: oklch(0.16 0.003 85);
--color-gtw20-mut: #615e57;
--color-gtw20-mut: oklch(0.47 0.01 85);
--color-gtw20-acc: #8a6314;
--color-gtw20-acc: oklch(0.5 0.1 78);
}
}@import "tailwindcss";
@theme {
--color-gtw20-bg: #0b0b0c;
--color-gtw20-bg: oklch(0.16 0.003 85);
--color-gtw20-surf: #16161a;
--color-gtw20-surf: oklch(0.22 0.005 85);
--color-gtw20-line: #292930;
--color-gtw20-line: oklch(0.31 0.008 85);
--color-gtw20-ink: #f2f1ee;
--color-gtw20-ink: oklch(0.96 0.005 85);
--color-gtw20-mut: #9b988f;
--color-gtw20-mut: oklch(0.69 0.01 85);
--color-gtw20-acc: #e5c07b;
--color-gtw20-acc: oklch(0.83 0.11 88);
--font-gtw20-display: 'Space Grotesk', 'Inter Tight', ui-sans-serif, system-ui, sans-serif;
--font-gtw20-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
/* The gate animation as a token: mints animate-gtw20-gate, keyframes below. */
--animate-gtw20-gate: gtw-20-gate 0.9s steps(8) infinite;
}
@keyframes gtw-20-gate {
0% {
transform: translate3d(0, 0, 0);
}
12% {
transform: translate3d(-3%, 2%, 0);
}
25% {
transform: translate3d(4%, -3%, 0);
}
37% {
transform: translate3d(-5%, -1%, 0);
}
50% {
transform: translate3d(2%, 4%, 0);
}
62% {
transform: translate3d(-2%, -4%, 0);
}
75% {
transform: translate3d(5%, 1%, 0);
}
87% {
transform: translate3d(-4%, 3%, 0);
}
100% {
transform: translate3d(0, 0, 0);
}
}
.gtw-20 {
width: 100%;
min-height: 100svh;
--gtw20-anim: 0.11;
--gtw20-static: 0.17;
--gtw20-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='gtw20n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23gtw20n)'/%3E%3C/svg%3E");
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.55;
}
.gtw-20:has(.gtw-20-theme:checked) {
--color-gtw20-bg: #f3f2ef;
--color-gtw20-bg: oklch(0.96 0.004 85);
--color-gtw20-surf: #ffffff;
--color-gtw20-surf: oklch(1 0 0);
--color-gtw20-line: #dedbd4;
--color-gtw20-line: oklch(0.89 0.008 85);
--color-gtw20-ink: #0b0b0c;
--color-gtw20-ink: oklch(0.16 0.003 85);
--color-gtw20-mut: #615e57;
--color-gtw20-mut: oklch(0.47 0.01 85);
--color-gtw20-acc: #8a6314;
--color-gtw20-acc: oklch(0.5 0.1 78);
}
@media (prefers-color-scheme: light) {
.gtw-20 {
--color-gtw20-bg: #f3f2ef;
--color-gtw20-bg: oklch(0.96 0.004 85);
--color-gtw20-surf: #ffffff;
--color-gtw20-surf: oklch(1 0 0);
--color-gtw20-line: #dedbd4;
--color-gtw20-line: oklch(0.89 0.008 85);
--color-gtw20-ink: #0b0b0c;
--color-gtw20-ink: oklch(0.16 0.003 85);
--color-gtw20-mut: #615e57;
--color-gtw20-mut: oklch(0.47 0.01 85);
--color-gtw20-acc: #8a6314;
--color-gtw20-acc: oklch(0.5 0.1 78);
}
.gtw-20:has(.gtw-20-theme:checked) {
--color-gtw20-bg: #0b0b0c;
--color-gtw20-bg: oklch(0.16 0.003 85);
--color-gtw20-surf: #16161a;
--color-gtw20-surf: oklch(0.22 0.005 85);
--color-gtw20-line: #292930;
--color-gtw20-line: oklch(0.31 0.008 85);
--color-gtw20-ink: #f2f1ee;
--color-gtw20-ink: oklch(0.96 0.005 85);
--color-gtw20-mut: #9b988f;
--color-gtw20-mut: oklch(0.69 0.01 85);
--color-gtw20-acc: #e5c07b;
--color-gtw20-acc: oklch(0.83 0.11 88);
}
}
@media (prefers-color-scheme: dark) {
.gtw-20:has(.gtw-20-theme:checked) {
--color-gtw20-bg: #f3f2ef;
--color-gtw20-bg: oklch(0.96 0.004 85);
--color-gtw20-surf: #ffffff;
--color-gtw20-surf: oklch(1 0 0);
--color-gtw20-line: #dedbd4;
--color-gtw20-line: oklch(0.89 0.008 85);
--color-gtw20-ink: #0b0b0c;
--color-gtw20-ink: oklch(0.16 0.003 85);
--color-gtw20-mut: #615e57;
--color-gtw20-mut: oklch(0.47 0.01 85);
--color-gtw20-acc: #8a6314;
--color-gtw20-acc: oklch(0.5 0.1 78);
}
}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-gtw20-bg: #0b0b0c;
--color-gtw20-bg: oklch(0.16 0.003 85);
--color-gtw20-surf: #16161a;
--color-gtw20-surf: oklch(0.22 0.005 85);
--color-gtw20-line: #292930;
--color-gtw20-line: oklch(0.31 0.008 85);
--color-gtw20-ink: #f2f1ee;
--color-gtw20-ink: oklch(0.96 0.005 85);
--color-gtw20-mut: #9b988f;
--color-gtw20-mut: oklch(0.69 0.01 85);
--color-gtw20-acc: #e5c07b;
--color-gtw20-acc: oklch(0.83 0.11 88);
--font-gtw20-display: 'Space Grotesk', 'Inter Tight', ui-sans-serif, system-ui, sans-serif;
--font-gtw20-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
/* The gate animation as a token: mints animate-gtw20-gate, keyframes below. */
--animate-gtw20-gate: gtw-20-gate 0.9s steps(8) infinite;
}
@keyframes gtw-20-gate {
0% {
transform: translate3d(0, 0, 0);
}
12% {
transform: translate3d(-3%, 2%, 0);
}
25% {
transform: translate3d(4%, -3%, 0);
}
37% {
transform: translate3d(-5%, -1%, 0);
}
50% {
transform: translate3d(2%, 4%, 0);
}
62% {
transform: translate3d(-2%, -4%, 0);
}
75% {
transform: translate3d(5%, 1%, 0);
}
87% {
transform: translate3d(-4%, 3%, 0);
}
100% {
transform: translate3d(0, 0, 0);
}
}
.gtw-20 {
width: 100%;
min-height: 100svh;
--gtw20-anim: 0.11;
--gtw20-static: 0.17;
--gtw20-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='gtw20n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23gtw20n)'/%3E%3C/svg%3E");
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.55;
}
.gtw-20:has(.gtw-20-theme:checked) {
--color-gtw20-bg: #f3f2ef;
--color-gtw20-bg: oklch(0.96 0.004 85);
--color-gtw20-surf: #ffffff;
--color-gtw20-surf: oklch(1 0 0);
--color-gtw20-line: #dedbd4;
--color-gtw20-line: oklch(0.89 0.008 85);
--color-gtw20-ink: #0b0b0c;
--color-gtw20-ink: oklch(0.16 0.003 85);
--color-gtw20-mut: #615e57;
--color-gtw20-mut: oklch(0.47 0.01 85);
--color-gtw20-acc: #8a6314;
--color-gtw20-acc: oklch(0.5 0.1 78);
}
@media (prefers-color-scheme: light) {
.gtw-20 {
--color-gtw20-bg: #f3f2ef;
--color-gtw20-bg: oklch(0.96 0.004 85);
--color-gtw20-surf: #ffffff;
--color-gtw20-surf: oklch(1 0 0);
--color-gtw20-line: #dedbd4;
--color-gtw20-line: oklch(0.89 0.008 85);
--color-gtw20-ink: #0b0b0c;
--color-gtw20-ink: oklch(0.16 0.003 85);
--color-gtw20-mut: #615e57;
--color-gtw20-mut: oklch(0.47 0.01 85);
--color-gtw20-acc: #8a6314;
--color-gtw20-acc: oklch(0.5 0.1 78);
}
.gtw-20:has(.gtw-20-theme:checked) {
--color-gtw20-bg: #0b0b0c;
--color-gtw20-bg: oklch(0.16 0.003 85);
--color-gtw20-surf: #16161a;
--color-gtw20-surf: oklch(0.22 0.005 85);
--color-gtw20-line: #292930;
--color-gtw20-line: oklch(0.31 0.008 85);
--color-gtw20-ink: #f2f1ee;
--color-gtw20-ink: oklch(0.96 0.005 85);
--color-gtw20-mut: #9b988f;
--color-gtw20-mut: oklch(0.69 0.01 85);
--color-gtw20-acc: #e5c07b;
--color-gtw20-acc: oklch(0.83 0.11 88);
}
}
@media (prefers-color-scheme: dark) {
.gtw-20:has(.gtw-20-theme:checked) {
--color-gtw20-bg: #f3f2ef;
--color-gtw20-bg: oklch(0.96 0.004 85);
--color-gtw20-surf: #ffffff;
--color-gtw20-surf: oklch(1 0 0);
--color-gtw20-line: #dedbd4;
--color-gtw20-line: oklch(0.89 0.008 85);
--color-gtw20-ink: #0b0b0c;
--color-gtw20-ink: oklch(0.16 0.003 85);
--color-gtw20-mut: #615e57;
--color-gtw20-mut: oklch(0.47 0.01 85);
--color-gtw20-acc: #8a6314;
--color-gtw20-acc: oklch(0.5 0.1 78);
}
}@import "tailwindcss";
@theme {
--color-gtw20-bg: #0b0b0c;
--color-gtw20-bg: oklch(0.16 0.003 85);
--color-gtw20-surf: #16161a;
--color-gtw20-surf: oklch(0.22 0.005 85);
--color-gtw20-line: #292930;
--color-gtw20-line: oklch(0.31 0.008 85);
--color-gtw20-ink: #f2f1ee;
--color-gtw20-ink: oklch(0.96 0.005 85);
--color-gtw20-mut: #9b988f;
--color-gtw20-mut: oklch(0.69 0.01 85);
--color-gtw20-acc: #e5c07b;
--color-gtw20-acc: oklch(0.83 0.11 88);
--font-gtw20-display: 'Space Grotesk', 'Inter Tight', ui-sans-serif, system-ui, sans-serif;
--font-gtw20-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
/* The gate animation as a token: mints animate-gtw20-gate, keyframes below. */
--animate-gtw20-gate: gtw-20-gate 0.9s steps(8) infinite;
}
@keyframes gtw-20-gate {
0% {
transform: translate3d(0, 0, 0);
}
12% {
transform: translate3d(-3%, 2%, 0);
}
25% {
transform: translate3d(4%, -3%, 0);
}
37% {
transform: translate3d(-5%, -1%, 0);
}
50% {
transform: translate3d(2%, 4%, 0);
}
62% {
transform: translate3d(-2%, -4%, 0);
}
75% {
transform: translate3d(5%, 1%, 0);
}
87% {
transform: translate3d(-4%, 3%, 0);
}
100% {
transform: translate3d(0, 0, 0);
}
}
.gtw-20 {
width: 100%;
min-height: 100svh;
--gtw20-anim: 0.11;
--gtw20-static: 0.17;
--gtw20-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='gtw20n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23gtw20n)'/%3E%3C/svg%3E");
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.55;
}
.gtw-20:has(.gtw-20-theme:checked) {
--color-gtw20-bg: #f3f2ef;
--color-gtw20-bg: oklch(0.96 0.004 85);
--color-gtw20-surf: #ffffff;
--color-gtw20-surf: oklch(1 0 0);
--color-gtw20-line: #dedbd4;
--color-gtw20-line: oklch(0.89 0.008 85);
--color-gtw20-ink: #0b0b0c;
--color-gtw20-ink: oklch(0.16 0.003 85);
--color-gtw20-mut: #615e57;
--color-gtw20-mut: oklch(0.47 0.01 85);
--color-gtw20-acc: #8a6314;
--color-gtw20-acc: oklch(0.5 0.1 78);
}
@media (prefers-color-scheme: light) {
.gtw-20 {
--color-gtw20-bg: #f3f2ef;
--color-gtw20-bg: oklch(0.96 0.004 85);
--color-gtw20-surf: #ffffff;
--color-gtw20-surf: oklch(1 0 0);
--color-gtw20-line: #dedbd4;
--color-gtw20-line: oklch(0.89 0.008 85);
--color-gtw20-ink: #0b0b0c;
--color-gtw20-ink: oklch(0.16 0.003 85);
--color-gtw20-mut: #615e57;
--color-gtw20-mut: oklch(0.47 0.01 85);
--color-gtw20-acc: #8a6314;
--color-gtw20-acc: oklch(0.5 0.1 78);
}
.gtw-20:has(.gtw-20-theme:checked) {
--color-gtw20-bg: #0b0b0c;
--color-gtw20-bg: oklch(0.16 0.003 85);
--color-gtw20-surf: #16161a;
--color-gtw20-surf: oklch(0.22 0.005 85);
--color-gtw20-line: #292930;
--color-gtw20-line: oklch(0.31 0.008 85);
--color-gtw20-ink: #f2f1ee;
--color-gtw20-ink: oklch(0.96 0.005 85);
--color-gtw20-mut: #9b988f;
--color-gtw20-mut: oklch(0.69 0.01 85);
--color-gtw20-acc: #e5c07b;
--color-gtw20-acc: oklch(0.83 0.11 88);
}
}
@media (prefers-color-scheme: dark) {
.gtw-20:has(.gtw-20-theme:checked) {
--color-gtw20-bg: #f3f2ef;
--color-gtw20-bg: oklch(0.96 0.004 85);
--color-gtw20-surf: #ffffff;
--color-gtw20-surf: oklch(1 0 0);
--color-gtw20-line: #dedbd4;
--color-gtw20-line: oklch(0.89 0.008 85);
--color-gtw20-ink: #0b0b0c;
--color-gtw20-ink: oklch(0.16 0.003 85);
--color-gtw20-mut: #615e57;
--color-gtw20-mut: oklch(0.47 0.01 85);
--color-gtw20-acc: #8a6314;
--color-gtw20-acc: oklch(0.5 0.1 78);
}
}<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&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&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The reduced-motion fallback is the whole port. A CSS
@media (prefers-reduced-motion: reduce)block with six overrides becomes sixmotion-reduce:utilities sitting beside the values they replace — so a reviewer can see the still frame's design without scrolling to the bottom of a stylesheet. hidden motion-reduce:inline/inline motion-reduce:hiddenon the two badge labels is the neatest instance: the page states which mode it is in, and the mechanism is two class lists rather than a display-toggling rule pair.- The gate animation is an
@themetoken (--animate-gtw20-gate) with hand-written@keyframes— the only correct split, since keyframes are not design-scale values. motion-reduce:will-change-autoreleases the compositing layer when nothing is animating. Easy to forget in CSS, hard to forget when it sits next towill-change-transform.- UI change from the CSS original: hero lede is full-opacity white rather than 0.86 alpha, vignette stops are
oklch(), and the theme toggle forwards focus — over a bright frame the faded lede was the collection's last remaining contrast risk.
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 Reduced Motion Static Grain Fallback
Source: https://codefronts.com/design-styles/tailwind-css-grain-texture/tailwind-reduced-motion-static-grain-fallback/
Animated grain is the single most likely thing in a texture collection to make somebody feel ill — rapid luminance change across a large area is exactly the trigger for vestibular disorders, migraine and photosensitivity. This demo animates a film gate properly (compositor transforms, steps(8)), and then designs the reduced-motion version instead of switching the animation off.
## HTML
```html
<section class="gtw-20 relative block w-full min-h-svh bg-gtw20-bg text-gtw20-ink" aria-labelledby="gtw-20-title">
<div class="absolute top-[clamp(1rem,3vw,1.75rem)] right-[clamp(0.85rem,3vw,1.75rem)] z-60">
<input type="checkbox" id="gtw-20-theme" class="gtw-20-theme peer sr-only">
<label for="gtw-20-theme" class="grid place-items-center w-11 h-11 rounded-full border border-white/30 bg-[oklch(0.14_0.004_85/0.5)] backdrop-blur-[8px] text-white cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-gtw20-acc peer-focus-visible:outline-offset-[3px]">
<svg viewBox="0 0 24 24" class="w-5 h-5" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="sr-only">Toggle dark theme</span>
</label>
</div>
<div class="relative isolate overflow-hidden grid items-end min-h-[min(82svh,42rem)] bg-[#0b0b0c]">
<img src="https://picsum.photos/id/1016/2000/1200" alt="" aria-hidden="true" width="2000" height="1200" class="absolute inset-0 z-0 w-full h-full object-cover [filter:grayscale(0.55)_contrast(1.12)_brightness(0.86)_sepia(0.12)] motion-reduce:[filter:grayscale(0.7)_contrast(1.18)_brightness(0.82)_sepia(0.16)]">
<span aria-hidden="true" class="absolute inset-0 z-[1] pointer-events-none opacity-70 motion-reduce:opacity-45 bg-[repeating-linear-gradient(0deg,rgba(255,255,255,0.035)_0_1px,transparent_1px_3px)]"></span>
<span aria-hidden="true" class="absolute -inset-[25%] z-[2] pointer-events-none bg-[image:var(--gtw20-noise)] opacity-[var(--gtw20-anim)] mix-blend-overlay animate-gtw20-gate will-change-transform motion-reduce:animate-none motion-reduce:inset-0 motion-reduce:opacity-[var(--gtw20-static)] motion-reduce:mix-blend-soft-light motion-reduce:will-change-auto"></span>
<span aria-hidden="true" class="absolute inset-0 z-[3] pointer-events-none bg-[radial-gradient(122%_100%_at_50%_44%,transparent_42%,oklch(0.12_0.003_85/0.35)_78%,oklch(0.12_0.003_85/0.66)_100%)] motion-reduce:bg-[radial-gradient(118%_100%_at_50%_42%,transparent_34%,oklch(0.12_0.003_85/0.44)_74%,oklch(0.12_0.003_85/0.78)_100%)]"></span>
<div class="relative z-[5] w-[min(72rem,calc(100%-clamp(1.5rem,5vw,4rem)))] mx-auto grid gap-[0.8rem] max-w-[42rem] py-[clamp(2.5rem,8vw,4.5rem)] text-white">
<p class="m-0 justify-self-start px-3 py-[0.35rem] rounded-full border border-white/30 bg-[oklch(0.12_0.003_85/0.5)] backdrop-blur-[5px] font-gtw20-mono text-[0.72rem] font-medium leading-[1.4] tracking-[0.1em] uppercase">
<span class="inline motion-reduce:hidden">Projector running · animated grain</span>
<span class="hidden motion-reduce:inline">Reduced motion · static frame held</span>
</p>
<h1 id="gtw-20-title" class="m-0 font-gtw20-display text-[clamp(2.2rem,7vw,4.4rem)] font-semibold leading-[0.98] tracking-[-0.04em] text-balance">Reel four, gate check</h1>
<p class="m-0 max-w-[34rem] text-white text-[clamp(0.96rem,2vw,1.11rem)] text-pretty">The grain layer translates on the compositor in eight discrete steps. If your system asks for reduced motion, it stops — and a heavier, deliberately composed static treatment takes over.</p>
</div>
</div>
<main class="w-[min(72rem,calc(100%-clamp(1.5rem,5vw,4rem)))] mx-auto py-[clamp(2.5rem,7vw,4rem)] grid gap-[clamp(1.6rem,4vw,2.4rem)]">
<section class="grid gap-4" aria-labelledby="gtw-20-h-rules">
<h2 id="gtw-20-h-rules" class="m-0 pb-[0.7rem] border-b border-gtw20-line font-gtw20-display text-[clamp(1.2rem,3vw,1.6rem)] font-semibold leading-tight tracking-[-0.025em]">Four rules for animated texture</h2>
<ul class="list-none m-0 p-0 grid gap-[clamp(0.7rem,1.8vw,1.1rem)] grid-cols-[repeat(auto-fit,minmax(min(100%,15rem),1fr))]">
<li class="relative isolate overflow-hidden grid content-start gap-[0.3rem] min-w-0 p-[clamp(1rem,2.4vw,1.4rem)] rounded-[14px] border border-gtw20-line bg-gtw20-surf after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw20-noise)] after:opacity-[0.05] after:mix-blend-overlay">
<span class="font-gtw20-mono text-[0.7rem] font-medium leading-none tracking-[0.14em] text-gtw20-acc">01</span>
<b class="font-gtw20-display text-[0.98rem] font-semibold leading-[1.25] tracking-[-0.01em] text-balance">Translate, never background-position</b>
<span class="text-gtw20-mut text-[0.85rem] text-pretty">One is a compositor transform. The other repaints a viewport-sized background every frame.</span>
</li>
<li class="relative isolate overflow-hidden grid content-start gap-[0.3rem] min-w-0 p-[clamp(1rem,2.4vw,1.4rem)] rounded-[14px] border border-gtw20-line bg-gtw20-surf after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw20-noise)] after:opacity-[0.05] after:mix-blend-overlay">
<span class="font-gtw20-mono text-[0.7rem] font-medium leading-none tracking-[0.14em] text-gtw20-acc">02</span>
<b class="font-gtw20-display text-[0.98rem] font-semibold leading-[1.25] tracking-[-0.01em] text-balance">steps(), never linear</b>
<span class="text-gtw20-mut text-[0.85rem] text-pretty">Film grain jumps between frames. A smooth slide reads as sandpaper being dragged.</span>
</li>
<li class="relative isolate overflow-hidden grid content-start gap-[0.3rem] min-w-0 p-[clamp(1rem,2.4vw,1.4rem)] rounded-[14px] border border-gtw20-line bg-gtw20-surf after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw20-noise)] after:opacity-[0.05] after:mix-blend-overlay">
<span class="font-gtw20-mono text-[0.7rem] font-medium leading-none tracking-[0.14em] text-gtw20-acc">03</span>
<b class="font-gtw20-display text-[0.98rem] font-semibold leading-[1.25] tracking-[-0.01em] text-balance">Never animate filter: url()</b>
<span class="text-gtw20-mut text-[0.85rem] text-pretty">The turbulence re-evaluates every frame. Animate a static texture layer instead.</span>
</li>
<li class="relative isolate overflow-hidden grid content-start gap-[0.3rem] min-w-0 p-[clamp(1rem,2.4vw,1.4rem)] rounded-[14px] border border-gtw20-line bg-gtw20-surf after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw20-noise)] after:opacity-[0.05] after:mix-blend-overlay">
<span class="font-gtw20-mono text-[0.7rem] font-medium leading-none tracking-[0.14em] text-gtw20-acc">04</span>
<b class="font-gtw20-display text-[0.98rem] font-semibold leading-[1.25] tracking-[-0.01em] text-balance">Design the fallback</b>
<span class="text-gtw20-mut text-[0.85rem] text-pretty">Not <code class="font-gtw20-mono text-[0.9em]">animation: none</code>. Heavier grain, a vignette, one composed frame — a real design.</span>
</li>
</ul>
</section>
<aside class="grid gap-2 p-[clamp(1.2rem,3vw,1.8rem)] rounded-[14px] border border-gtw20-line bg-[color-mix(in_oklab,var(--color-gtw20-acc)_8%,transparent)]">
<h2 class="m-0 font-gtw20-display text-[1.05rem] font-semibold leading-tight">Why flicker is the specific risk</h2>
<p class="m-0 max-w-[52rem] text-gtw20-mut text-[0.92rem] text-pretty">Vestibular disorders, migraine and photosensitivity respond to rapid luminance change over large areas more reliably than to movement. Animated grain is precisely that. If it flickers across a large surface, honour <code class="font-gtw20-mono text-[0.9em]">prefers-reduced-motion</code> without exception — and give those users a treatment you would be happy to ship as the only one.</p>
</aside>
</main>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-gtw20-bg: #0b0b0c;
--color-gtw20-bg: oklch(0.16 0.003 85);
--color-gtw20-surf: #16161a;
--color-gtw20-surf: oklch(0.22 0.005 85);
--color-gtw20-line: #292930;
--color-gtw20-line: oklch(0.31 0.008 85);
--color-gtw20-ink: #f2f1ee;
--color-gtw20-ink: oklch(0.96 0.005 85);
--color-gtw20-mut: #9b988f;
--color-gtw20-mut: oklch(0.69 0.01 85);
--color-gtw20-acc: #e5c07b;
--color-gtw20-acc: oklch(0.83 0.11 88);
--font-gtw20-display: 'Space Grotesk', 'Inter Tight', ui-sans-serif, system-ui, sans-serif;
--font-gtw20-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
/* The gate animation as a token: mints animate-gtw20-gate, keyframes below. */
--animate-gtw20-gate: gtw-20-gate 0.9s steps(8) infinite;
}
@keyframes gtw-20-gate {
0% {
transform: translate3d(0, 0, 0);
}
12% {
transform: translate3d(-3%, 2%, 0);
}
25% {
transform: translate3d(4%, -3%, 0);
}
37% {
transform: translate3d(-5%, -1%, 0);
}
50% {
transform: translate3d(2%, 4%, 0);
}
62% {
transform: translate3d(-2%, -4%, 0);
}
75% {
transform: translate3d(5%, 1%, 0);
}
87% {
transform: translate3d(-4%, 3%, 0);
}
100% {
transform: translate3d(0, 0, 0);
}
}
.gtw-20 {
width: 100%;
min-height: 100svh;
--gtw20-anim: 0.11;
--gtw20-static: 0.17;
--gtw20-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='gtw20n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23gtw20n)'/%3E%3C/svg%3E");
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.55;
}
.gtw-20:has(.gtw-20-theme:checked) {
--color-gtw20-bg: #f3f2ef;
--color-gtw20-bg: oklch(0.96 0.004 85);
--color-gtw20-surf: #ffffff;
--color-gtw20-surf: oklch(1 0 0);
--color-gtw20-line: #dedbd4;
--color-gtw20-line: oklch(0.89 0.008 85);
--color-gtw20-ink: #0b0b0c;
--color-gtw20-ink: oklch(0.16 0.003 85);
--color-gtw20-mut: #615e57;
--color-gtw20-mut: oklch(0.47 0.01 85);
--color-gtw20-acc: #8a6314;
--color-gtw20-acc: oklch(0.5 0.1 78);
}
@media (prefers-color-scheme: light) {
.gtw-20 {
--color-gtw20-bg: #f3f2ef;
--color-gtw20-bg: oklch(0.96 0.004 85);
--color-gtw20-surf: #ffffff;
--color-gtw20-surf: oklch(1 0 0);
--color-gtw20-line: #dedbd4;
--color-gtw20-line: oklch(0.89 0.008 85);
--color-gtw20-ink: #0b0b0c;
--color-gtw20-ink: oklch(0.16 0.003 85);
--color-gtw20-mut: #615e57;
--color-gtw20-mut: oklch(0.47 0.01 85);
--color-gtw20-acc: #8a6314;
--color-gtw20-acc: oklch(0.5 0.1 78);
}
.gtw-20:has(.gtw-20-theme:checked) {
--color-gtw20-bg: #0b0b0c;
--color-gtw20-bg: oklch(0.16 0.003 85);
--color-gtw20-surf: #16161a;
--color-gtw20-surf: oklch(0.22 0.005 85);
--color-gtw20-line: #292930;
--color-gtw20-line: oklch(0.31 0.008 85);
--color-gtw20-ink: #f2f1ee;
--color-gtw20-ink: oklch(0.96 0.005 85);
--color-gtw20-mut: #9b988f;
--color-gtw20-mut: oklch(0.69 0.01 85);
--color-gtw20-acc: #e5c07b;
--color-gtw20-acc: oklch(0.83 0.11 88);
}
}
@media (prefers-color-scheme: dark) {
.gtw-20:has(.gtw-20-theme:checked) {
--color-gtw20-bg: #f3f2ef;
--color-gtw20-bg: oklch(0.96 0.004 85);
--color-gtw20-surf: #ffffff;
--color-gtw20-surf: oklch(1 0 0);
--color-gtw20-line: #dedbd4;
--color-gtw20-line: oklch(0.89 0.008 85);
--color-gtw20-ink: #0b0b0c;
--color-gtw20-ink: oklch(0.16 0.003 85);
--color-gtw20-mut: #615e57;
--color-gtw20-mut: oklch(0.47 0.01 85);
--color-gtw20-acc: #8a6314;
--color-gtw20-acc: oklch(0.5 0.1 78);
}
}
```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 Reduced Motion Static Grain Fallback
Source: https://codefronts.com/design-styles/tailwind-css-grain-texture/tailwind-reduced-motion-static-grain-fallback/
Animated grain is the single most likely thing in a texture collection to make somebody feel ill — rapid luminance change across a large area is exactly the trigger for vestibular disorders, migraine and photosensitivity. This demo animates a film gate properly (compositor transforms, steps(8)), and then designs the reduced-motion version instead of switching the animation off.
## HTML
```html
<section class="gtw-20 relative block w-full min-h-svh bg-gtw20-bg text-gtw20-ink" aria-labelledby="gtw-20-title">
<div class="absolute top-[clamp(1rem,3vw,1.75rem)] right-[clamp(0.85rem,3vw,1.75rem)] z-60">
<input type="checkbox" id="gtw-20-theme" class="gtw-20-theme peer sr-only">
<label for="gtw-20-theme" class="grid place-items-center w-11 h-11 rounded-full border border-white/30 bg-[oklch(0.14_0.004_85/0.5)] backdrop-blur-[8px] text-white cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-gtw20-acc peer-focus-visible:outline-offset-[3px]">
<svg viewBox="0 0 24 24" class="w-5 h-5" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="sr-only">Toggle dark theme</span>
</label>
</div>
<div class="relative isolate overflow-hidden grid items-end min-h-[min(82svh,42rem)] bg-[#0b0b0c]">
<img src="https://picsum.photos/id/1016/2000/1200" alt="" aria-hidden="true" width="2000" height="1200" class="absolute inset-0 z-0 w-full h-full object-cover [filter:grayscale(0.55)_contrast(1.12)_brightness(0.86)_sepia(0.12)] motion-reduce:[filter:grayscale(0.7)_contrast(1.18)_brightness(0.82)_sepia(0.16)]">
<span aria-hidden="true" class="absolute inset-0 z-[1] pointer-events-none opacity-70 motion-reduce:opacity-45 bg-[repeating-linear-gradient(0deg,rgba(255,255,255,0.035)_0_1px,transparent_1px_3px)]"></span>
<span aria-hidden="true" class="absolute -inset-[25%] z-[2] pointer-events-none bg-[image:var(--gtw20-noise)] opacity-[var(--gtw20-anim)] mix-blend-overlay animate-gtw20-gate will-change-transform motion-reduce:animate-none motion-reduce:inset-0 motion-reduce:opacity-[var(--gtw20-static)] motion-reduce:mix-blend-soft-light motion-reduce:will-change-auto"></span>
<span aria-hidden="true" class="absolute inset-0 z-[3] pointer-events-none bg-[radial-gradient(122%_100%_at_50%_44%,transparent_42%,oklch(0.12_0.003_85/0.35)_78%,oklch(0.12_0.003_85/0.66)_100%)] motion-reduce:bg-[radial-gradient(118%_100%_at_50%_42%,transparent_34%,oklch(0.12_0.003_85/0.44)_74%,oklch(0.12_0.003_85/0.78)_100%)]"></span>
<div class="relative z-[5] w-[min(72rem,calc(100%-clamp(1.5rem,5vw,4rem)))] mx-auto grid gap-[0.8rem] max-w-[42rem] py-[clamp(2.5rem,8vw,4.5rem)] text-white">
<p class="m-0 justify-self-start px-3 py-[0.35rem] rounded-full border border-white/30 bg-[oklch(0.12_0.003_85/0.5)] backdrop-blur-[5px] font-gtw20-mono text-[0.72rem] font-medium leading-[1.4] tracking-[0.1em] uppercase">
<span class="inline motion-reduce:hidden">Projector running · animated grain</span>
<span class="hidden motion-reduce:inline">Reduced motion · static frame held</span>
</p>
<h1 id="gtw-20-title" class="m-0 font-gtw20-display text-[clamp(2.2rem,7vw,4.4rem)] font-semibold leading-[0.98] tracking-[-0.04em] text-balance">Reel four, gate check</h1>
<p class="m-0 max-w-[34rem] text-white text-[clamp(0.96rem,2vw,1.11rem)] text-pretty">The grain layer translates on the compositor in eight discrete steps. If your system asks for reduced motion, it stops — and a heavier, deliberately composed static treatment takes over.</p>
</div>
</div>
<main class="w-[min(72rem,calc(100%-clamp(1.5rem,5vw,4rem)))] mx-auto py-[clamp(2.5rem,7vw,4rem)] grid gap-[clamp(1.6rem,4vw,2.4rem)]">
<section class="grid gap-4" aria-labelledby="gtw-20-h-rules">
<h2 id="gtw-20-h-rules" class="m-0 pb-[0.7rem] border-b border-gtw20-line font-gtw20-display text-[clamp(1.2rem,3vw,1.6rem)] font-semibold leading-tight tracking-[-0.025em]">Four rules for animated texture</h2>
<ul class="list-none m-0 p-0 grid gap-[clamp(0.7rem,1.8vw,1.1rem)] grid-cols-[repeat(auto-fit,minmax(min(100%,15rem),1fr))]">
<li class="relative isolate overflow-hidden grid content-start gap-[0.3rem] min-w-0 p-[clamp(1rem,2.4vw,1.4rem)] rounded-[14px] border border-gtw20-line bg-gtw20-surf after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw20-noise)] after:opacity-[0.05] after:mix-blend-overlay">
<span class="font-gtw20-mono text-[0.7rem] font-medium leading-none tracking-[0.14em] text-gtw20-acc">01</span>
<b class="font-gtw20-display text-[0.98rem] font-semibold leading-[1.25] tracking-[-0.01em] text-balance">Translate, never background-position</b>
<span class="text-gtw20-mut text-[0.85rem] text-pretty">One is a compositor transform. The other repaints a viewport-sized background every frame.</span>
</li>
<li class="relative isolate overflow-hidden grid content-start gap-[0.3rem] min-w-0 p-[clamp(1rem,2.4vw,1.4rem)] rounded-[14px] border border-gtw20-line bg-gtw20-surf after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw20-noise)] after:opacity-[0.05] after:mix-blend-overlay">
<span class="font-gtw20-mono text-[0.7rem] font-medium leading-none tracking-[0.14em] text-gtw20-acc">02</span>
<b class="font-gtw20-display text-[0.98rem] font-semibold leading-[1.25] tracking-[-0.01em] text-balance">steps(), never linear</b>
<span class="text-gtw20-mut text-[0.85rem] text-pretty">Film grain jumps between frames. A smooth slide reads as sandpaper being dragged.</span>
</li>
<li class="relative isolate overflow-hidden grid content-start gap-[0.3rem] min-w-0 p-[clamp(1rem,2.4vw,1.4rem)] rounded-[14px] border border-gtw20-line bg-gtw20-surf after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw20-noise)] after:opacity-[0.05] after:mix-blend-overlay">
<span class="font-gtw20-mono text-[0.7rem] font-medium leading-none tracking-[0.14em] text-gtw20-acc">03</span>
<b class="font-gtw20-display text-[0.98rem] font-semibold leading-[1.25] tracking-[-0.01em] text-balance">Never animate filter: url()</b>
<span class="text-gtw20-mut text-[0.85rem] text-pretty">The turbulence re-evaluates every frame. Animate a static texture layer instead.</span>
</li>
<li class="relative isolate overflow-hidden grid content-start gap-[0.3rem] min-w-0 p-[clamp(1rem,2.4vw,1.4rem)] rounded-[14px] border border-gtw20-line bg-gtw20-surf after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw20-noise)] after:opacity-[0.05] after:mix-blend-overlay">
<span class="font-gtw20-mono text-[0.7rem] font-medium leading-none tracking-[0.14em] text-gtw20-acc">04</span>
<b class="font-gtw20-display text-[0.98rem] font-semibold leading-[1.25] tracking-[-0.01em] text-balance">Design the fallback</b>
<span class="text-gtw20-mut text-[0.85rem] text-pretty">Not <code class="font-gtw20-mono text-[0.9em]">animation: none</code>. Heavier grain, a vignette, one composed frame — a real design.</span>
</li>
</ul>
</section>
<aside class="grid gap-2 p-[clamp(1.2rem,3vw,1.8rem)] rounded-[14px] border border-gtw20-line bg-[color-mix(in_oklab,var(--color-gtw20-acc)_8%,transparent)]">
<h2 class="m-0 font-gtw20-display text-[1.05rem] font-semibold leading-tight">Why flicker is the specific risk</h2>
<p class="m-0 max-w-[52rem] text-gtw20-mut text-[0.92rem] text-pretty">Vestibular disorders, migraine and photosensitivity respond to rapid luminance change over large areas more reliably than to movement. Animated grain is precisely that. If it flickers across a large surface, honour <code class="font-gtw20-mono text-[0.9em]">prefers-reduced-motion</code> without exception — and give those users a treatment you would be happy to ship as the only one.</p>
</aside>
</main>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-gtw20-bg: #0b0b0c;
--color-gtw20-bg: oklch(0.16 0.003 85);
--color-gtw20-surf: #16161a;
--color-gtw20-surf: oklch(0.22 0.005 85);
--color-gtw20-line: #292930;
--color-gtw20-line: oklch(0.31 0.008 85);
--color-gtw20-ink: #f2f1ee;
--color-gtw20-ink: oklch(0.96 0.005 85);
--color-gtw20-mut: #9b988f;
--color-gtw20-mut: oklch(0.69 0.01 85);
--color-gtw20-acc: #e5c07b;
--color-gtw20-acc: oklch(0.83 0.11 88);
--font-gtw20-display: 'Space Grotesk', 'Inter Tight', ui-sans-serif, system-ui, sans-serif;
--font-gtw20-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
/* The gate animation as a token: mints animate-gtw20-gate, keyframes below. */
--animate-gtw20-gate: gtw-20-gate 0.9s steps(8) infinite;
}
@keyframes gtw-20-gate {
0% {
transform: translate3d(0, 0, 0);
}
12% {
transform: translate3d(-3%, 2%, 0);
}
25% {
transform: translate3d(4%, -3%, 0);
}
37% {
transform: translate3d(-5%, -1%, 0);
}
50% {
transform: translate3d(2%, 4%, 0);
}
62% {
transform: translate3d(-2%, -4%, 0);
}
75% {
transform: translate3d(5%, 1%, 0);
}
87% {
transform: translate3d(-4%, 3%, 0);
}
100% {
transform: translate3d(0, 0, 0);
}
}
.gtw-20 {
width: 100%;
min-height: 100svh;
--gtw20-anim: 0.11;
--gtw20-static: 0.17;
--gtw20-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='gtw20n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23gtw20n)'/%3E%3C/svg%3E");
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.55;
}
.gtw-20:has(.gtw-20-theme:checked) {
--color-gtw20-bg: #f3f2ef;
--color-gtw20-bg: oklch(0.96 0.004 85);
--color-gtw20-surf: #ffffff;
--color-gtw20-surf: oklch(1 0 0);
--color-gtw20-line: #dedbd4;
--color-gtw20-line: oklch(0.89 0.008 85);
--color-gtw20-ink: #0b0b0c;
--color-gtw20-ink: oklch(0.16 0.003 85);
--color-gtw20-mut: #615e57;
--color-gtw20-mut: oklch(0.47 0.01 85);
--color-gtw20-acc: #8a6314;
--color-gtw20-acc: oklch(0.5 0.1 78);
}
@media (prefers-color-scheme: light) {
.gtw-20 {
--color-gtw20-bg: #f3f2ef;
--color-gtw20-bg: oklch(0.96 0.004 85);
--color-gtw20-surf: #ffffff;
--color-gtw20-surf: oklch(1 0 0);
--color-gtw20-line: #dedbd4;
--color-gtw20-line: oklch(0.89 0.008 85);
--color-gtw20-ink: #0b0b0c;
--color-gtw20-ink: oklch(0.16 0.003 85);
--color-gtw20-mut: #615e57;
--color-gtw20-mut: oklch(0.47 0.01 85);
--color-gtw20-acc: #8a6314;
--color-gtw20-acc: oklch(0.5 0.1 78);
}
.gtw-20:has(.gtw-20-theme:checked) {
--color-gtw20-bg: #0b0b0c;
--color-gtw20-bg: oklch(0.16 0.003 85);
--color-gtw20-surf: #16161a;
--color-gtw20-surf: oklch(0.22 0.005 85);
--color-gtw20-line: #292930;
--color-gtw20-line: oklch(0.31 0.008 85);
--color-gtw20-ink: #f2f1ee;
--color-gtw20-ink: oklch(0.96 0.005 85);
--color-gtw20-mut: #9b988f;
--color-gtw20-mut: oklch(0.69 0.01 85);
--color-gtw20-acc: #e5c07b;
--color-gtw20-acc: oklch(0.83 0.11 88);
}
}
@media (prefers-color-scheme: dark) {
.gtw-20:has(.gtw-20-theme:checked) {
--color-gtw20-bg: #f3f2ef;
--color-gtw20-bg: oklch(0.96 0.004 85);
--color-gtw20-surf: #ffffff;
--color-gtw20-surf: oklch(1 0 0);
--color-gtw20-line: #dedbd4;
--color-gtw20-line: oklch(0.89 0.008 85);
--color-gtw20-ink: #0b0b0c;
--color-gtw20-ink: oklch(0.16 0.003 85);
--color-gtw20-mut: #615e57;
--color-gtw20-mut: oklch(0.47 0.01 85);
--color-gtw20-acc: #8a6314;
--color-gtw20-acc: oklch(0.5 0.1 78);
}
}
```How this works
Animate translate, never background-position. A transform runs on the compositor and costs nothing per frame; animating background-position repaints a viewport-sized background every single frame. Same visual result, wildly different cost — and on a mid-range phone the second one drops frames.
steps(8), never linear. Real film grain jumps between frames because each frame is a physically different piece of emulsion. A smooth slide reads as sandpaper being dragged across the image. Eight discrete positions per 0.9s is the whole difference between "projector" and "CSS demo".
The layer is oversized on purpose. -inset-[25%] means translating by ±5% never exposes an edge. Under reduced motion it snaps back to inset-0, because nothing moves and the overhang is wasted texture.
The fallback is a design, not an absence. animation: none leaves a thin, purposeless grain layer that was tuned for motion. This version instead raises grain to 0.17, switches to soft-light, softens the scan weave, deepens the vignette, and pushes the photograph further toward monochrome — one composed still frame. Ask yourself whether you would ship the reduced-motion version as your only version; if not, it is not finished.
Make it yours
- Gate speed:
--gtw20-gate(0.9s) withsteps(8). Faster than about 0.5s starts to flicker rather than shimmer. - Weights:
--gtw20-anim(0.11) and--gtw20-static(0.17). The static frame needs more grain because it has no movement to carry attention. - Steps: 8 is film-like. 4 is a hand-cranked projector; 24 is close enough to linear that you lose the effect.
- Scan weave: the 3px
repeating-linear-gradient. Drop it entirely for a digital rather than optical look. - Travel: ±5% in the keyframes. Larger values start reading as a shaking image rather than a moving gate.
Gotchas — read before shipping
- Never animate
filter: url()grain — the turbulence re-evaluates every frame and will drop frames on any mid-range device. Animate a static texture layer instead. prefers-reduced-motionis not a performance hint, it is an accessibility requirement. Honour it without exception on anything that flickers across a large area.- A reduced-motion fallback of
animation: noneleaves a layer tuned for motion sitting still. Design the still frame deliberately, as this demo does. will-change: transformleft on permanently keeps a compositing layer alive and costs memory. Scope it to the animated element and release it undermotion-reduce:.- Percentage translates on an oversized layer are relative to the layer, not the viewport — which is why
-inset-[25%]and ±5% travel are paired.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
prefers-reduced-motion is supported in Chrome 74+, Safari 10.1+ and Firefox 63+, and defaults to no-preference where unknown — so the animated path is the fallback, not the reverse. The quoted floor is Tailwind v4's generated sheet.