19 / 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 Dark Mode Noise Without Muddy Shadows
Grain gets ripped back out of dark products for one reason: overlay can only darken below mid-grey, so noise on a near-black surface turns elevation into mud. The fix is three parts — soft-light instead of overlay, roughly 1.6× the light-theme opacity, and a mask that keeps noise out of the deepest shadows. A dark dashboard shipping all three.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="gtw-19 relative block w-full min-h-svh bg-gtw19-bg text-gtw19-ink" aria-labelledby="gtw-19-title">
<div class="absolute top-[clamp(1rem,3vw,1.75rem)] right-[clamp(0.85rem,3vw,1.75rem)] z-60">
<input type="checkbox" id="gtw-19-theme" class="gtw-19-theme peer sr-only">
<label for="gtw-19-theme" class="grid place-items-center w-11 h-11 rounded-full border border-gtw19-line bg-gtw19-s3 text-gtw19-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-gtw19-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 light theme</span>
</label>
</div>
<div class="gtw-19-app grid grid-cols-[minmax(0,1fr)] min-[62rem]:grid-cols-[16rem_minmax(0,1fr)] min-h-svh">
<aside aria-label="Sections" class="gtw-19-rail relative isolate overflow-hidden grid content-start gap-[1.2rem] p-[clamp(1.2rem,3vw,1.8rem)] border-r border-gtw19-line bg-gtw19-s1">
<p class="m-0 font-gtw19-display text-[1.05rem] font-semibold leading-none tracking-[-0.01em]">◐ Umbra</p>
<nav class="grid gap-[0.15rem]">
<a href="#gtw-19-title" aria-current="page" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink aria-[current=page]:bg-gtw19-s3 aria-[current=page]:text-gtw19-ink aria-[current=page]:shadow-[inset_2px_0_0_var(--color-gtw19-acc)] focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Overview</a>
<a href="#gtw-19-title" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Surfaces</a>
<a href="#gtw-19-title" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Elevation</a>
<a href="#gtw-19-title" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Contrast</a>
<a href="#gtw-19-title" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Tokens</a>
</nav>
<p class="m-0 pt-4 border-t border-gtw19-line text-gtw19-mut text-[0.8rem] text-pretty">Grain weight climbs with elevation. Shadows do almost nothing here.</p>
</aside>
<main class="grid content-start gap-[clamp(1.4rem,3.5vw,2.2rem)] p-[clamp(1.5rem,4vw,2.6rem)] pr-[clamp(4rem,8vw,5rem)]">
<header class="max-w-[46rem] grid gap-[0.55rem]">
<p class="m-0 font-gtw19-mono text-[0.72rem] font-medium leading-none tracking-[0.18em] uppercase text-gtw19-acc">Dark surface recipe · 19</p>
<h1 id="gtw-19-title" class="m-0 font-gtw19-display text-[clamp(1.8rem,4.6vw,2.9rem)] font-semibold leading-[1.05] tracking-[-0.035em] text-balance">Texture in the dark, without the mud</h1>
<p class="m-0 max-w-[40rem] text-gtw19-mut text-[clamp(0.93rem,1.8vw,1.04rem)] text-pretty">Two grain weights, <code class="font-gtw19-mono text-[0.9em]">soft-light</code> instead of <code class="font-gtw19-mono text-[0.9em]">overlay</code>, and a mask that keeps noise out of the deepest shadows. Toggle the theme and watch both numbers change.</p>
</header>
<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%,11rem),1fr))]">
<li class="relative isolate overflow-hidden grid gap-[0.15rem] min-w-0 px-[1.15rem] py-4 rounded-[14px] border border-gtw19-line bg-gtw19-s1 transition-[translate] duration-[260ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g1)]">
<span class="font-gtw19-mono text-[0.7rem] leading-none tracking-[0.12em] uppercase text-gtw19-mut">Surface 1</span>
<span class="font-gtw19-display text-[1.5rem] font-semibold leading-[1.1] tracking-[-0.03em]">8% L</span>
<span class="font-gtw19-mono text-[0.76rem] leading-none text-gtw19-acc">grain 0.055</span>
</li>
<li class="relative isolate overflow-hidden grid gap-[0.15rem] min-w-0 px-[1.15rem] py-4 rounded-[14px] border border-gtw19-line bg-gtw19-s2 transition-[translate] duration-[260ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g2)]">
<span class="font-gtw19-mono text-[0.7rem] leading-none tracking-[0.12em] uppercase text-gtw19-mut">Surface 2</span>
<span class="font-gtw19-display text-[1.5rem] font-semibold leading-[1.1] tracking-[-0.03em]">12% L</span>
<span class="font-gtw19-mono text-[0.76rem] leading-none text-gtw19-acc">grain 0.075</span>
</li>
<li class="relative isolate overflow-hidden grid gap-[0.15rem] min-w-0 px-[1.15rem] py-4 rounded-[14px] border border-gtw19-line bg-gtw19-s3 transition-[translate] duration-[260ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g3)]">
<span class="font-gtw19-mono text-[0.7rem] leading-none tracking-[0.12em] uppercase text-gtw19-mut">Surface 3</span>
<span class="font-gtw19-display text-[1.5rem] font-semibold leading-[1.1] tracking-[-0.03em]">17% L</span>
<span class="font-gtw19-mono text-[0.76rem] leading-none text-gtw19-acc">grain 0.095</span>
</li>
</ul>
<div class="grid gap-[clamp(0.8rem,2vw,1.2rem)] grid-cols-[repeat(auto-fit,minmax(min(100%,17rem),1fr))] items-start">
<section class="col-span-full relative isolate overflow-hidden grid content-start gap-[0.8rem] min-w-0 p-[clamp(1.1rem,2.6vw,1.5rem)] rounded-2xl border border-gtw19-line bg-gtw19-s2 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g2)]" aria-labelledby="gtw-19-h-chart">
<div class="flex items-center justify-between gap-[0.6rem] pb-[0.6rem] border-b border-gtw19-line">
<h2 id="gtw-19-h-chart" class="m-0 font-gtw19-display text-[1rem] font-semibold leading-tight tracking-[-0.015em]">Perceived texture vs opacity</h2>
<span class="px-2 py-[0.15rem] rounded-full bg-[color-mix(in_oklab,var(--color-gtw19-acc)_18%,transparent)] text-gtw19-acc font-gtw19-mono text-[0.66rem] font-medium leading-[1.6] tracking-[0.1em] uppercase">measured</span>
</div>
<div role="img" aria-label="Bar chart showing dark surfaces need roughly 1.6 times the grain opacity of light surfaces for equal perceived texture" class="grid grid-flow-col auto-cols-fr items-end gap-[clamp(0.4rem,1.5vw,0.9rem)] h-[clamp(7rem,16vw,10rem)] pt-2">
<span class="relative block h-[32%] rounded-t-lg bg-[color-mix(in_oklab,var(--color-gtw19-acc)_28%,var(--color-gtw19-s3))]"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-mut">0.03</b></span>
<span class="relative block h-[48%] rounded-t-lg bg-[color-mix(in_oklab,var(--color-gtw19-acc)_28%,var(--color-gtw19-s3))]"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-mut">0.045</b></span>
<span class="relative block h-[68%] rounded-t-lg bg-gtw19-acc"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-ink">0.06</b></span>
<span class="relative block h-[84%] rounded-t-lg bg-gtw19-acc"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-ink">0.075</b></span>
<span class="relative block h-full rounded-t-lg bg-[color-mix(in_oklab,var(--color-gtw19-acc)_28%,var(--color-gtw19-s3))]"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-mut">0.095</b></span>
</div>
<p class="m-0 text-gtw19-mut text-[0.83rem] text-pretty">The marked pair is the working range for dark product UI. The same perceived texture on a light surface sits at 0.04–0.05.</p>
</section>
<section class="relative isolate overflow-hidden grid content-start gap-[0.8rem] min-w-0 p-[clamp(1.1rem,2.6vw,1.5rem)] rounded-2xl border border-gtw19-line bg-gtw19-s2 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g2)]" aria-labelledby="gtw-19-h-blend">
<div class="flex items-center justify-between gap-[0.6rem] pb-[0.6rem] border-b border-gtw19-line"><h2 id="gtw-19-h-blend" class="m-0 font-gtw19-display text-[1rem] font-semibold leading-tight tracking-[-0.015em]">Blend choice</h2></div>
<ul class="list-none m-0 p-0 grid gap-[0.1rem]">
<li class="grid gap-[0.1rem] py-[0.55rem]"><b class="font-gtw19-mono text-[0.88rem] font-medium leading-tight">soft-light</b><span class="text-gtw19-mut text-[0.82rem] text-pretty">Ship this. Roughly symmetric at low lightness.</span></li>
<li class="grid gap-[0.1rem] py-[0.55rem] border-t border-gtw19-line"><b class="font-gtw19-mono text-[0.88rem] font-medium leading-tight">screen</b><span class="text-gtw19-mut text-[0.82rem] text-pretty">For true black. Can only lift, never crush.</span></li>
<li class="grid gap-[0.1rem] py-[0.55rem] border-t border-gtw19-line"><b class="font-gtw19-mono text-[0.88rem] font-medium leading-tight">overlay</b><span class="text-gtw19-mut text-[0.82rem] text-pretty">Avoid. Only darkens below mid-grey — this is the mud.</span></li>
</ul>
</section>
<section class="relative isolate overflow-hidden grid content-start gap-[0.8rem] min-w-0 p-[clamp(1.1rem,2.6vw,1.5rem)] rounded-2xl border border-gtw19-line bg-gtw19-s2 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g2)]" aria-labelledby="gtw-19-h-contrast">
<div class="flex items-center justify-between gap-[0.6rem] pb-[0.6rem] border-b border-gtw19-line"><h2 id="gtw-19-h-contrast" class="m-0 font-gtw19-display text-[1rem] font-semibold leading-tight tracking-[-0.015em]">Contrast, composited</h2></div>
<p class="m-0 text-gtw19-mut text-[0.83rem] text-pretty">A 9% grain layer measurably reduces text contrast. Check the composited surface, not the flat token.</p>
<dl class="grid gap-0 m-0">
<div class="flex items-baseline justify-between gap-[0.6rem] py-2 border-t border-gtw19-line"><dt class="m-0 text-gtw19-mut text-[0.83rem]">Body on surface 2</dt><dd class="m-0 font-gtw19-mono text-[0.9rem] font-semibold leading-none tabular-nums">7.1:1</dd></div>
<div class="flex items-baseline justify-between gap-[0.6rem] py-2 border-t border-gtw19-line"><dt class="m-0 text-gtw19-mut text-[0.83rem]">Same, grain applied</dt><dd class="m-0 font-gtw19-mono text-[0.9rem] font-semibold leading-none tabular-nums">6.8:1</dd></div>
<div class="flex items-baseline justify-between gap-[0.6rem] py-2 border-t border-gtw19-line"><dt class="m-0 text-gtw19-mut text-[0.83rem]">Muted on surface 2</dt><dd class="m-0 font-gtw19-mono text-[0.9rem] font-semibold leading-none tabular-nums">4.7:1</dd></div>
</dl>
</section>
</div>
</main>
</div>
</section><section class="gtw-19 relative block w-full min-h-svh bg-gtw19-bg text-gtw19-ink" aria-labelledby="gtw-19-title">
<div class="absolute top-[clamp(1rem,3vw,1.75rem)] right-[clamp(0.85rem,3vw,1.75rem)] z-60">
<input type="checkbox" id="gtw-19-theme" class="gtw-19-theme peer sr-only">
<label for="gtw-19-theme" class="grid place-items-center w-11 h-11 rounded-full border border-gtw19-line bg-gtw19-s3 text-gtw19-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-gtw19-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 light theme</span>
</label>
</div>
<div class="gtw-19-app grid grid-cols-[minmax(0,1fr)] min-[62rem]:grid-cols-[16rem_minmax(0,1fr)] min-h-svh">
<aside aria-label="Sections" class="gtw-19-rail relative isolate overflow-hidden grid content-start gap-[1.2rem] p-[clamp(1.2rem,3vw,1.8rem)] border-r border-gtw19-line bg-gtw19-s1">
<p class="m-0 font-gtw19-display text-[1.05rem] font-semibold leading-none tracking-[-0.01em]">◐ Umbra</p>
<nav class="grid gap-[0.15rem]">
<a href="#gtw-19-title" aria-current="page" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink aria-[current=page]:bg-gtw19-s3 aria-[current=page]:text-gtw19-ink aria-[current=page]:shadow-[inset_2px_0_0_var(--color-gtw19-acc)] focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Overview</a>
<a href="#gtw-19-title" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Surfaces</a>
<a href="#gtw-19-title" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Elevation</a>
<a href="#gtw-19-title" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Contrast</a>
<a href="#gtw-19-title" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Tokens</a>
</nav>
<p class="m-0 pt-4 border-t border-gtw19-line text-gtw19-mut text-[0.8rem] text-pretty">Grain weight climbs with elevation. Shadows do almost nothing here.</p>
</aside>
<main class="grid content-start gap-[clamp(1.4rem,3.5vw,2.2rem)] p-[clamp(1.5rem,4vw,2.6rem)] pr-[clamp(4rem,8vw,5rem)]">
<header class="max-w-[46rem] grid gap-[0.55rem]">
<p class="m-0 font-gtw19-mono text-[0.72rem] font-medium leading-none tracking-[0.18em] uppercase text-gtw19-acc">Dark surface recipe · 19</p>
<h1 id="gtw-19-title" class="m-0 font-gtw19-display text-[clamp(1.8rem,4.6vw,2.9rem)] font-semibold leading-[1.05] tracking-[-0.035em] text-balance">Texture in the dark, without the mud</h1>
<p class="m-0 max-w-[40rem] text-gtw19-mut text-[clamp(0.93rem,1.8vw,1.04rem)] text-pretty">Two grain weights, <code class="font-gtw19-mono text-[0.9em]">soft-light</code> instead of <code class="font-gtw19-mono text-[0.9em]">overlay</code>, and a mask that keeps noise out of the deepest shadows. Toggle the theme and watch both numbers change.</p>
</header>
<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%,11rem),1fr))]">
<li class="relative isolate overflow-hidden grid gap-[0.15rem] min-w-0 px-[1.15rem] py-4 rounded-[14px] border border-gtw19-line bg-gtw19-s1 transition-[translate] duration-[260ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g1)]">
<span class="font-gtw19-mono text-[0.7rem] leading-none tracking-[0.12em] uppercase text-gtw19-mut">Surface 1</span>
<span class="font-gtw19-display text-[1.5rem] font-semibold leading-[1.1] tracking-[-0.03em]">8% L</span>
<span class="font-gtw19-mono text-[0.76rem] leading-none text-gtw19-acc">grain 0.055</span>
</li>
<li class="relative isolate overflow-hidden grid gap-[0.15rem] min-w-0 px-[1.15rem] py-4 rounded-[14px] border border-gtw19-line bg-gtw19-s2 transition-[translate] duration-[260ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g2)]">
<span class="font-gtw19-mono text-[0.7rem] leading-none tracking-[0.12em] uppercase text-gtw19-mut">Surface 2</span>
<span class="font-gtw19-display text-[1.5rem] font-semibold leading-[1.1] tracking-[-0.03em]">12% L</span>
<span class="font-gtw19-mono text-[0.76rem] leading-none text-gtw19-acc">grain 0.075</span>
</li>
<li class="relative isolate overflow-hidden grid gap-[0.15rem] min-w-0 px-[1.15rem] py-4 rounded-[14px] border border-gtw19-line bg-gtw19-s3 transition-[translate] duration-[260ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g3)]">
<span class="font-gtw19-mono text-[0.7rem] leading-none tracking-[0.12em] uppercase text-gtw19-mut">Surface 3</span>
<span class="font-gtw19-display text-[1.5rem] font-semibold leading-[1.1] tracking-[-0.03em]">17% L</span>
<span class="font-gtw19-mono text-[0.76rem] leading-none text-gtw19-acc">grain 0.095</span>
</li>
</ul>
<div class="grid gap-[clamp(0.8rem,2vw,1.2rem)] grid-cols-[repeat(auto-fit,minmax(min(100%,17rem),1fr))] items-start">
<section class="col-span-full relative isolate overflow-hidden grid content-start gap-[0.8rem] min-w-0 p-[clamp(1.1rem,2.6vw,1.5rem)] rounded-2xl border border-gtw19-line bg-gtw19-s2 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g2)]" aria-labelledby="gtw-19-h-chart">
<div class="flex items-center justify-between gap-[0.6rem] pb-[0.6rem] border-b border-gtw19-line">
<h2 id="gtw-19-h-chart" class="m-0 font-gtw19-display text-[1rem] font-semibold leading-tight tracking-[-0.015em]">Perceived texture vs opacity</h2>
<span class="px-2 py-[0.15rem] rounded-full bg-[color-mix(in_oklab,var(--color-gtw19-acc)_18%,transparent)] text-gtw19-acc font-gtw19-mono text-[0.66rem] font-medium leading-[1.6] tracking-[0.1em] uppercase">measured</span>
</div>
<div role="img" aria-label="Bar chart showing dark surfaces need roughly 1.6 times the grain opacity of light surfaces for equal perceived texture" class="grid grid-flow-col auto-cols-fr items-end gap-[clamp(0.4rem,1.5vw,0.9rem)] h-[clamp(7rem,16vw,10rem)] pt-2">
<span class="relative block h-[32%] rounded-t-lg bg-[color-mix(in_oklab,var(--color-gtw19-acc)_28%,var(--color-gtw19-s3))]"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-mut">0.03</b></span>
<span class="relative block h-[48%] rounded-t-lg bg-[color-mix(in_oklab,var(--color-gtw19-acc)_28%,var(--color-gtw19-s3))]"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-mut">0.045</b></span>
<span class="relative block h-[68%] rounded-t-lg bg-gtw19-acc"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-ink">0.06</b></span>
<span class="relative block h-[84%] rounded-t-lg bg-gtw19-acc"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-ink">0.075</b></span>
<span class="relative block h-full rounded-t-lg bg-[color-mix(in_oklab,var(--color-gtw19-acc)_28%,var(--color-gtw19-s3))]"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-mut">0.095</b></span>
</div>
<p class="m-0 text-gtw19-mut text-[0.83rem] text-pretty">The marked pair is the working range for dark product UI. The same perceived texture on a light surface sits at 0.04–0.05.</p>
</section>
<section class="relative isolate overflow-hidden grid content-start gap-[0.8rem] min-w-0 p-[clamp(1.1rem,2.6vw,1.5rem)] rounded-2xl border border-gtw19-line bg-gtw19-s2 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g2)]" aria-labelledby="gtw-19-h-blend">
<div class="flex items-center justify-between gap-[0.6rem] pb-[0.6rem] border-b border-gtw19-line"><h2 id="gtw-19-h-blend" class="m-0 font-gtw19-display text-[1rem] font-semibold leading-tight tracking-[-0.015em]">Blend choice</h2></div>
<ul class="list-none m-0 p-0 grid gap-[0.1rem]">
<li class="grid gap-[0.1rem] py-[0.55rem]"><b class="font-gtw19-mono text-[0.88rem] font-medium leading-tight">soft-light</b><span class="text-gtw19-mut text-[0.82rem] text-pretty">Ship this. Roughly symmetric at low lightness.</span></li>
<li class="grid gap-[0.1rem] py-[0.55rem] border-t border-gtw19-line"><b class="font-gtw19-mono text-[0.88rem] font-medium leading-tight">screen</b><span class="text-gtw19-mut text-[0.82rem] text-pretty">For true black. Can only lift, never crush.</span></li>
<li class="grid gap-[0.1rem] py-[0.55rem] border-t border-gtw19-line"><b class="font-gtw19-mono text-[0.88rem] font-medium leading-tight">overlay</b><span class="text-gtw19-mut text-[0.82rem] text-pretty">Avoid. Only darkens below mid-grey — this is the mud.</span></li>
</ul>
</section>
<section class="relative isolate overflow-hidden grid content-start gap-[0.8rem] min-w-0 p-[clamp(1.1rem,2.6vw,1.5rem)] rounded-2xl border border-gtw19-line bg-gtw19-s2 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g2)]" aria-labelledby="gtw-19-h-contrast">
<div class="flex items-center justify-between gap-[0.6rem] pb-[0.6rem] border-b border-gtw19-line"><h2 id="gtw-19-h-contrast" class="m-0 font-gtw19-display text-[1rem] font-semibold leading-tight tracking-[-0.015em]">Contrast, composited</h2></div>
<p class="m-0 text-gtw19-mut text-[0.83rem] text-pretty">A 9% grain layer measurably reduces text contrast. Check the composited surface, not the flat token.</p>
<dl class="grid gap-0 m-0">
<div class="flex items-baseline justify-between gap-[0.6rem] py-2 border-t border-gtw19-line"><dt class="m-0 text-gtw19-mut text-[0.83rem]">Body on surface 2</dt><dd class="m-0 font-gtw19-mono text-[0.9rem] font-semibold leading-none tabular-nums">7.1:1</dd></div>
<div class="flex items-baseline justify-between gap-[0.6rem] py-2 border-t border-gtw19-line"><dt class="m-0 text-gtw19-mut text-[0.83rem]">Same, grain applied</dt><dd class="m-0 font-gtw19-mono text-[0.9rem] font-semibold leading-none tabular-nums">6.8:1</dd></div>
<div class="flex items-baseline justify-between gap-[0.6rem] py-2 border-t border-gtw19-line"><dt class="m-0 text-gtw19-mut text-[0.83rem]">Muted on surface 2</dt><dd class="m-0 font-gtw19-mono text-[0.9rem] font-semibold leading-none tabular-nums">4.7:1</dd></div>
</dl>
</section>
</div>
</main>
</div>
</section>@import "tailwindcss";
@theme {
--color-gtw19-bg: #08090b;
--color-gtw19-bg: oklch(0.14 0.005 265);
--color-gtw19-s1: #101216;
--color-gtw19-s1: oklch(0.19 0.008 265);
--color-gtw19-s2: #171a20;
--color-gtw19-s2: oklch(0.23 0.01 265);
--color-gtw19-s3: #20242c;
--color-gtw19-s3: oklch(0.28 0.012 265);
--color-gtw19-line: #262b34;
--color-gtw19-line: oklch(0.31 0.012 265);
--color-gtw19-ink: #eef1f6;
--color-gtw19-ink: oklch(0.95 0.008 265);
--color-gtw19-mut: #98a1b0;
--color-gtw19-mut: oklch(0.7 0.02 265);
--color-gtw19-acc: #7dd3fc;
--color-gtw19-acc: oklch(0.84 0.1 226);
--font-gtw19-display: 'Space Grotesk', 'Inter Tight', ui-sans-serif, system-ui, sans-serif;
--font-gtw19-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
.gtw-19 {
width: 100%;
min-height: 100svh;
--gtw19-g1: 0.055;
--gtw19-g2: 0.075;
--gtw19-g3: 0.095;
--gtw19-blend: soft-light;
--gtw19-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='gtw19n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23gtw19n)'/%3E%3C/svg%3E");
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.55;
}
.gtw-19-app {
min-height: 100vh;
min-height: 100svh;
}
/* The rail's grain fades out over the bottom 30%, where its shadow gradient lives — noise in
the deepest shadow is what kills elevation definition on dark UI. The -webkit- twin is
required for older Safari and has no utility form. */
.gtw-19-rail::after {
content: "";
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background-image: var(--gtw19-noise);
opacity: var(--gtw19-g1);
mix-blend-mode: var(--gtw19-blend);
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 70%, rgba(0, 0, 0, 0.35) 100%);
mask-image: linear-gradient(to bottom, #000 0%, #000 70%, rgba(0, 0, 0, 0.35) 100%);
}
.gtw-19:has(.gtw-19-theme:checked) {
--color-gtw19-bg: #f4f6fa;
--color-gtw19-bg: oklch(0.97 0.005 265);
--color-gtw19-s1: #ffffff;
--color-gtw19-s1: oklch(1 0 0);
--color-gtw19-s2: #f7f9fc;
--color-gtw19-s2: oklch(0.98 0.004 265);
--color-gtw19-s3: #eef2f8;
--color-gtw19-s3: oklch(0.95 0.008 265);
--color-gtw19-line: #dde3ec;
--color-gtw19-line: oklch(0.9 0.01 265);
--color-gtw19-ink: #0b0d11;
--color-gtw19-ink: oklch(0.16 0.008 265);
--color-gtw19-mut: #5b6474;
--color-gtw19-mut: oklch(0.47 0.02 265);
--color-gtw19-acc: #0369a1;
--color-gtw19-acc: oklch(0.48 0.11 235);
--gtw19-g1: 0.03;
--gtw19-g2: 0.045;
--gtw19-g3: 0.055;
--gtw19-blend: multiply;
}
@media (prefers-color-scheme: light) {
.gtw-19 {
--color-gtw19-bg: #f4f6fa;
--color-gtw19-bg: oklch(0.97 0.005 265);
--color-gtw19-s1: #ffffff;
--color-gtw19-s1: oklch(1 0 0);
--color-gtw19-s2: #f7f9fc;
--color-gtw19-s2: oklch(0.98 0.004 265);
--color-gtw19-s3: #eef2f8;
--color-gtw19-s3: oklch(0.95 0.008 265);
--color-gtw19-line: #dde3ec;
--color-gtw19-line: oklch(0.9 0.01 265);
--color-gtw19-ink: #0b0d11;
--color-gtw19-ink: oklch(0.16 0.008 265);
--color-gtw19-mut: #5b6474;
--color-gtw19-mut: oklch(0.47 0.02 265);
--color-gtw19-acc: #0369a1;
--color-gtw19-acc: oklch(0.48 0.11 235);
--gtw19-g1: 0.03;
--gtw19-g2: 0.045;
--gtw19-g3: 0.055;
--gtw19-blend: multiply;
}
.gtw-19:has(.gtw-19-theme:checked) {
--color-gtw19-bg: #08090b;
--color-gtw19-bg: oklch(0.14 0.005 265);
--color-gtw19-s1: #101216;
--color-gtw19-s1: oklch(0.19 0.008 265);
--color-gtw19-s2: #171a20;
--color-gtw19-s2: oklch(0.23 0.01 265);
--color-gtw19-s3: #20242c;
--color-gtw19-s3: oklch(0.28 0.012 265);
--color-gtw19-line: #262b34;
--color-gtw19-line: oklch(0.31 0.012 265);
--color-gtw19-ink: #eef1f6;
--color-gtw19-ink: oklch(0.95 0.008 265);
--color-gtw19-mut: #98a1b0;
--color-gtw19-mut: oklch(0.7 0.02 265);
--color-gtw19-acc: #7dd3fc;
--color-gtw19-acc: oklch(0.84 0.1 226);
--gtw19-g1: 0.055;
--gtw19-g2: 0.075;
--gtw19-g3: 0.095;
--gtw19-blend: soft-light;
}
}
@media (prefers-color-scheme: dark) {
.gtw-19:has(.gtw-19-theme:checked) {
--color-gtw19-bg: #f4f6fa;
--color-gtw19-bg: oklch(0.97 0.005 265);
--color-gtw19-s1: #ffffff;
--color-gtw19-s1: oklch(1 0 0);
--color-gtw19-s2: #f7f9fc;
--color-gtw19-s2: oklch(0.98 0.004 265);
--color-gtw19-s3: #eef2f8;
--color-gtw19-s3: oklch(0.95 0.008 265);
--color-gtw19-line: #dde3ec;
--color-gtw19-line: oklch(0.9 0.01 265);
--color-gtw19-ink: #0b0d11;
--color-gtw19-ink: oklch(0.16 0.008 265);
--color-gtw19-mut: #5b6474;
--color-gtw19-mut: oklch(0.47 0.02 265);
--color-gtw19-acc: #0369a1;
--color-gtw19-acc: oklch(0.48 0.11 235);
--gtw19-g1: 0.03;
--gtw19-g2: 0.045;
--gtw19-g3: 0.055;
--gtw19-blend: multiply;
}
}@import "tailwindcss";
@theme {
--color-gtw19-bg: #08090b;
--color-gtw19-bg: oklch(0.14 0.005 265);
--color-gtw19-s1: #101216;
--color-gtw19-s1: oklch(0.19 0.008 265);
--color-gtw19-s2: #171a20;
--color-gtw19-s2: oklch(0.23 0.01 265);
--color-gtw19-s3: #20242c;
--color-gtw19-s3: oklch(0.28 0.012 265);
--color-gtw19-line: #262b34;
--color-gtw19-line: oklch(0.31 0.012 265);
--color-gtw19-ink: #eef1f6;
--color-gtw19-ink: oklch(0.95 0.008 265);
--color-gtw19-mut: #98a1b0;
--color-gtw19-mut: oklch(0.7 0.02 265);
--color-gtw19-acc: #7dd3fc;
--color-gtw19-acc: oklch(0.84 0.1 226);
--font-gtw19-display: 'Space Grotesk', 'Inter Tight', ui-sans-serif, system-ui, sans-serif;
--font-gtw19-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
.gtw-19 {
width: 100%;
min-height: 100svh;
--gtw19-g1: 0.055;
--gtw19-g2: 0.075;
--gtw19-g3: 0.095;
--gtw19-blend: soft-light;
--gtw19-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='gtw19n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23gtw19n)'/%3E%3C/svg%3E");
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.55;
}
.gtw-19-app {
min-height: 100vh;
min-height: 100svh;
}
/* The rail's grain fades out over the bottom 30%, where its shadow gradient lives — noise in
the deepest shadow is what kills elevation definition on dark UI. The -webkit- twin is
required for older Safari and has no utility form. */
.gtw-19-rail::after {
content: "";
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background-image: var(--gtw19-noise);
opacity: var(--gtw19-g1);
mix-blend-mode: var(--gtw19-blend);
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 70%, rgba(0, 0, 0, 0.35) 100%);
mask-image: linear-gradient(to bottom, #000 0%, #000 70%, rgba(0, 0, 0, 0.35) 100%);
}
.gtw-19:has(.gtw-19-theme:checked) {
--color-gtw19-bg: #f4f6fa;
--color-gtw19-bg: oklch(0.97 0.005 265);
--color-gtw19-s1: #ffffff;
--color-gtw19-s1: oklch(1 0 0);
--color-gtw19-s2: #f7f9fc;
--color-gtw19-s2: oklch(0.98 0.004 265);
--color-gtw19-s3: #eef2f8;
--color-gtw19-s3: oklch(0.95 0.008 265);
--color-gtw19-line: #dde3ec;
--color-gtw19-line: oklch(0.9 0.01 265);
--color-gtw19-ink: #0b0d11;
--color-gtw19-ink: oklch(0.16 0.008 265);
--color-gtw19-mut: #5b6474;
--color-gtw19-mut: oklch(0.47 0.02 265);
--color-gtw19-acc: #0369a1;
--color-gtw19-acc: oklch(0.48 0.11 235);
--gtw19-g1: 0.03;
--gtw19-g2: 0.045;
--gtw19-g3: 0.055;
--gtw19-blend: multiply;
}
@media (prefers-color-scheme: light) {
.gtw-19 {
--color-gtw19-bg: #f4f6fa;
--color-gtw19-bg: oklch(0.97 0.005 265);
--color-gtw19-s1: #ffffff;
--color-gtw19-s1: oklch(1 0 0);
--color-gtw19-s2: #f7f9fc;
--color-gtw19-s2: oklch(0.98 0.004 265);
--color-gtw19-s3: #eef2f8;
--color-gtw19-s3: oklch(0.95 0.008 265);
--color-gtw19-line: #dde3ec;
--color-gtw19-line: oklch(0.9 0.01 265);
--color-gtw19-ink: #0b0d11;
--color-gtw19-ink: oklch(0.16 0.008 265);
--color-gtw19-mut: #5b6474;
--color-gtw19-mut: oklch(0.47 0.02 265);
--color-gtw19-acc: #0369a1;
--color-gtw19-acc: oklch(0.48 0.11 235);
--gtw19-g1: 0.03;
--gtw19-g2: 0.045;
--gtw19-g3: 0.055;
--gtw19-blend: multiply;
}
.gtw-19:has(.gtw-19-theme:checked) {
--color-gtw19-bg: #08090b;
--color-gtw19-bg: oklch(0.14 0.005 265);
--color-gtw19-s1: #101216;
--color-gtw19-s1: oklch(0.19 0.008 265);
--color-gtw19-s2: #171a20;
--color-gtw19-s2: oklch(0.23 0.01 265);
--color-gtw19-s3: #20242c;
--color-gtw19-s3: oklch(0.28 0.012 265);
--color-gtw19-line: #262b34;
--color-gtw19-line: oklch(0.31 0.012 265);
--color-gtw19-ink: #eef1f6;
--color-gtw19-ink: oklch(0.95 0.008 265);
--color-gtw19-mut: #98a1b0;
--color-gtw19-mut: oklch(0.7 0.02 265);
--color-gtw19-acc: #7dd3fc;
--color-gtw19-acc: oklch(0.84 0.1 226);
--gtw19-g1: 0.055;
--gtw19-g2: 0.075;
--gtw19-g3: 0.095;
--gtw19-blend: soft-light;
}
}
@media (prefers-color-scheme: dark) {
.gtw-19:has(.gtw-19-theme:checked) {
--color-gtw19-bg: #f4f6fa;
--color-gtw19-bg: oklch(0.97 0.005 265);
--color-gtw19-s1: #ffffff;
--color-gtw19-s1: oklch(1 0 0);
--color-gtw19-s2: #f7f9fc;
--color-gtw19-s2: oklch(0.98 0.004 265);
--color-gtw19-s3: #eef2f8;
--color-gtw19-s3: oklch(0.95 0.008 265);
--color-gtw19-line: #dde3ec;
--color-gtw19-line: oklch(0.9 0.01 265);
--color-gtw19-ink: #0b0d11;
--color-gtw19-ink: oklch(0.16 0.008 265);
--color-gtw19-mut: #5b6474;
--color-gtw19-mut: oklch(0.47 0.02 265);
--color-gtw19-acc: #0369a1;
--color-gtw19-acc: oklch(0.48 0.11 235);
--gtw19-g1: 0.03;
--gtw19-g2: 0.045;
--gtw19-g3: 0.055;
--gtw19-blend: multiply;
}
}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-gtw19-bg: #08090b;
--color-gtw19-bg: oklch(0.14 0.005 265);
--color-gtw19-s1: #101216;
--color-gtw19-s1: oklch(0.19 0.008 265);
--color-gtw19-s2: #171a20;
--color-gtw19-s2: oklch(0.23 0.01 265);
--color-gtw19-s3: #20242c;
--color-gtw19-s3: oklch(0.28 0.012 265);
--color-gtw19-line: #262b34;
--color-gtw19-line: oklch(0.31 0.012 265);
--color-gtw19-ink: #eef1f6;
--color-gtw19-ink: oklch(0.95 0.008 265);
--color-gtw19-mut: #98a1b0;
--color-gtw19-mut: oklch(0.7 0.02 265);
--color-gtw19-acc: #7dd3fc;
--color-gtw19-acc: oklch(0.84 0.1 226);
--font-gtw19-display: 'Space Grotesk', 'Inter Tight', ui-sans-serif, system-ui, sans-serif;
--font-gtw19-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
.gtw-19 {
width: 100%;
min-height: 100svh;
--gtw19-g1: 0.055;
--gtw19-g2: 0.075;
--gtw19-g3: 0.095;
--gtw19-blend: soft-light;
--gtw19-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='gtw19n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23gtw19n)'/%3E%3C/svg%3E");
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.55;
}
.gtw-19-app {
min-height: 100vh;
min-height: 100svh;
}
/* The rail's grain fades out over the bottom 30%, where its shadow gradient lives — noise in
the deepest shadow is what kills elevation definition on dark UI. The -webkit- twin is
required for older Safari and has no utility form. */
.gtw-19-rail::after {
content: "";
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background-image: var(--gtw19-noise);
opacity: var(--gtw19-g1);
mix-blend-mode: var(--gtw19-blend);
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 70%, rgba(0, 0, 0, 0.35) 100%);
mask-image: linear-gradient(to bottom, #000 0%, #000 70%, rgba(0, 0, 0, 0.35) 100%);
}
.gtw-19:has(.gtw-19-theme:checked) {
--color-gtw19-bg: #f4f6fa;
--color-gtw19-bg: oklch(0.97 0.005 265);
--color-gtw19-s1: #ffffff;
--color-gtw19-s1: oklch(1 0 0);
--color-gtw19-s2: #f7f9fc;
--color-gtw19-s2: oklch(0.98 0.004 265);
--color-gtw19-s3: #eef2f8;
--color-gtw19-s3: oklch(0.95 0.008 265);
--color-gtw19-line: #dde3ec;
--color-gtw19-line: oklch(0.9 0.01 265);
--color-gtw19-ink: #0b0d11;
--color-gtw19-ink: oklch(0.16 0.008 265);
--color-gtw19-mut: #5b6474;
--color-gtw19-mut: oklch(0.47 0.02 265);
--color-gtw19-acc: #0369a1;
--color-gtw19-acc: oklch(0.48 0.11 235);
--gtw19-g1: 0.03;
--gtw19-g2: 0.045;
--gtw19-g3: 0.055;
--gtw19-blend: multiply;
}
@media (prefers-color-scheme: light) {
.gtw-19 {
--color-gtw19-bg: #f4f6fa;
--color-gtw19-bg: oklch(0.97 0.005 265);
--color-gtw19-s1: #ffffff;
--color-gtw19-s1: oklch(1 0 0);
--color-gtw19-s2: #f7f9fc;
--color-gtw19-s2: oklch(0.98 0.004 265);
--color-gtw19-s3: #eef2f8;
--color-gtw19-s3: oklch(0.95 0.008 265);
--color-gtw19-line: #dde3ec;
--color-gtw19-line: oklch(0.9 0.01 265);
--color-gtw19-ink: #0b0d11;
--color-gtw19-ink: oklch(0.16 0.008 265);
--color-gtw19-mut: #5b6474;
--color-gtw19-mut: oklch(0.47 0.02 265);
--color-gtw19-acc: #0369a1;
--color-gtw19-acc: oklch(0.48 0.11 235);
--gtw19-g1: 0.03;
--gtw19-g2: 0.045;
--gtw19-g3: 0.055;
--gtw19-blend: multiply;
}
.gtw-19:has(.gtw-19-theme:checked) {
--color-gtw19-bg: #08090b;
--color-gtw19-bg: oklch(0.14 0.005 265);
--color-gtw19-s1: #101216;
--color-gtw19-s1: oklch(0.19 0.008 265);
--color-gtw19-s2: #171a20;
--color-gtw19-s2: oklch(0.23 0.01 265);
--color-gtw19-s3: #20242c;
--color-gtw19-s3: oklch(0.28 0.012 265);
--color-gtw19-line: #262b34;
--color-gtw19-line: oklch(0.31 0.012 265);
--color-gtw19-ink: #eef1f6;
--color-gtw19-ink: oklch(0.95 0.008 265);
--color-gtw19-mut: #98a1b0;
--color-gtw19-mut: oklch(0.7 0.02 265);
--color-gtw19-acc: #7dd3fc;
--color-gtw19-acc: oklch(0.84 0.1 226);
--gtw19-g1: 0.055;
--gtw19-g2: 0.075;
--gtw19-g3: 0.095;
--gtw19-blend: soft-light;
}
}
@media (prefers-color-scheme: dark) {
.gtw-19:has(.gtw-19-theme:checked) {
--color-gtw19-bg: #f4f6fa;
--color-gtw19-bg: oklch(0.97 0.005 265);
--color-gtw19-s1: #ffffff;
--color-gtw19-s1: oklch(1 0 0);
--color-gtw19-s2: #f7f9fc;
--color-gtw19-s2: oklch(0.98 0.004 265);
--color-gtw19-s3: #eef2f8;
--color-gtw19-s3: oklch(0.95 0.008 265);
--color-gtw19-line: #dde3ec;
--color-gtw19-line: oklch(0.9 0.01 265);
--color-gtw19-ink: #0b0d11;
--color-gtw19-ink: oklch(0.16 0.008 265);
--color-gtw19-mut: #5b6474;
--color-gtw19-mut: oklch(0.47 0.02 265);
--color-gtw19-acc: #0369a1;
--color-gtw19-acc: oklch(0.48 0.11 235);
--gtw19-g1: 0.03;
--gtw19-g2: 0.045;
--gtw19-g3: 0.055;
--gtw19-blend: multiply;
}
}@import "tailwindcss";
@theme {
--color-gtw19-bg: #08090b;
--color-gtw19-bg: oklch(0.14 0.005 265);
--color-gtw19-s1: #101216;
--color-gtw19-s1: oklch(0.19 0.008 265);
--color-gtw19-s2: #171a20;
--color-gtw19-s2: oklch(0.23 0.01 265);
--color-gtw19-s3: #20242c;
--color-gtw19-s3: oklch(0.28 0.012 265);
--color-gtw19-line: #262b34;
--color-gtw19-line: oklch(0.31 0.012 265);
--color-gtw19-ink: #eef1f6;
--color-gtw19-ink: oklch(0.95 0.008 265);
--color-gtw19-mut: #98a1b0;
--color-gtw19-mut: oklch(0.7 0.02 265);
--color-gtw19-acc: #7dd3fc;
--color-gtw19-acc: oklch(0.84 0.1 226);
--font-gtw19-display: 'Space Grotesk', 'Inter Tight', ui-sans-serif, system-ui, sans-serif;
--font-gtw19-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
.gtw-19 {
width: 100%;
min-height: 100svh;
--gtw19-g1: 0.055;
--gtw19-g2: 0.075;
--gtw19-g3: 0.095;
--gtw19-blend: soft-light;
--gtw19-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='gtw19n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23gtw19n)'/%3E%3C/svg%3E");
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.55;
}
.gtw-19-app {
min-height: 100vh;
min-height: 100svh;
}
/* The rail's grain fades out over the bottom 30%, where its shadow gradient lives — noise in
the deepest shadow is what kills elevation definition on dark UI. The -webkit- twin is
required for older Safari and has no utility form. */
.gtw-19-rail::after {
content: "";
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background-image: var(--gtw19-noise);
opacity: var(--gtw19-g1);
mix-blend-mode: var(--gtw19-blend);
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 70%, rgba(0, 0, 0, 0.35) 100%);
mask-image: linear-gradient(to bottom, #000 0%, #000 70%, rgba(0, 0, 0, 0.35) 100%);
}
.gtw-19:has(.gtw-19-theme:checked) {
--color-gtw19-bg: #f4f6fa;
--color-gtw19-bg: oklch(0.97 0.005 265);
--color-gtw19-s1: #ffffff;
--color-gtw19-s1: oklch(1 0 0);
--color-gtw19-s2: #f7f9fc;
--color-gtw19-s2: oklch(0.98 0.004 265);
--color-gtw19-s3: #eef2f8;
--color-gtw19-s3: oklch(0.95 0.008 265);
--color-gtw19-line: #dde3ec;
--color-gtw19-line: oklch(0.9 0.01 265);
--color-gtw19-ink: #0b0d11;
--color-gtw19-ink: oklch(0.16 0.008 265);
--color-gtw19-mut: #5b6474;
--color-gtw19-mut: oklch(0.47 0.02 265);
--color-gtw19-acc: #0369a1;
--color-gtw19-acc: oklch(0.48 0.11 235);
--gtw19-g1: 0.03;
--gtw19-g2: 0.045;
--gtw19-g3: 0.055;
--gtw19-blend: multiply;
}
@media (prefers-color-scheme: light) {
.gtw-19 {
--color-gtw19-bg: #f4f6fa;
--color-gtw19-bg: oklch(0.97 0.005 265);
--color-gtw19-s1: #ffffff;
--color-gtw19-s1: oklch(1 0 0);
--color-gtw19-s2: #f7f9fc;
--color-gtw19-s2: oklch(0.98 0.004 265);
--color-gtw19-s3: #eef2f8;
--color-gtw19-s3: oklch(0.95 0.008 265);
--color-gtw19-line: #dde3ec;
--color-gtw19-line: oklch(0.9 0.01 265);
--color-gtw19-ink: #0b0d11;
--color-gtw19-ink: oklch(0.16 0.008 265);
--color-gtw19-mut: #5b6474;
--color-gtw19-mut: oklch(0.47 0.02 265);
--color-gtw19-acc: #0369a1;
--color-gtw19-acc: oklch(0.48 0.11 235);
--gtw19-g1: 0.03;
--gtw19-g2: 0.045;
--gtw19-g3: 0.055;
--gtw19-blend: multiply;
}
.gtw-19:has(.gtw-19-theme:checked) {
--color-gtw19-bg: #08090b;
--color-gtw19-bg: oklch(0.14 0.005 265);
--color-gtw19-s1: #101216;
--color-gtw19-s1: oklch(0.19 0.008 265);
--color-gtw19-s2: #171a20;
--color-gtw19-s2: oklch(0.23 0.01 265);
--color-gtw19-s3: #20242c;
--color-gtw19-s3: oklch(0.28 0.012 265);
--color-gtw19-line: #262b34;
--color-gtw19-line: oklch(0.31 0.012 265);
--color-gtw19-ink: #eef1f6;
--color-gtw19-ink: oklch(0.95 0.008 265);
--color-gtw19-mut: #98a1b0;
--color-gtw19-mut: oklch(0.7 0.02 265);
--color-gtw19-acc: #7dd3fc;
--color-gtw19-acc: oklch(0.84 0.1 226);
--gtw19-g1: 0.055;
--gtw19-g2: 0.075;
--gtw19-g3: 0.095;
--gtw19-blend: soft-light;
}
}
@media (prefers-color-scheme: dark) {
.gtw-19:has(.gtw-19-theme:checked) {
--color-gtw19-bg: #f4f6fa;
--color-gtw19-bg: oklch(0.97 0.005 265);
--color-gtw19-s1: #ffffff;
--color-gtw19-s1: oklch(1 0 0);
--color-gtw19-s2: #f7f9fc;
--color-gtw19-s2: oklch(0.98 0.004 265);
--color-gtw19-s3: #eef2f8;
--color-gtw19-s3: oklch(0.95 0.008 265);
--color-gtw19-line: #dde3ec;
--color-gtw19-line: oklch(0.9 0.01 265);
--color-gtw19-ink: #0b0d11;
--color-gtw19-ink: oklch(0.16 0.008 265);
--color-gtw19-mut: #5b6474;
--color-gtw19-mut: oklch(0.47 0.02 265);
--color-gtw19-acc: #0369a1;
--color-gtw19-acc: oklch(0.48 0.11 235);
--gtw19-g1: 0.03;
--gtw19-g2: 0.045;
--gtw19-g3: 0.055;
--gtw19-blend: multiply;
}
}<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.
aria-[current=page]:replaces[aria-current="page"]as three utilities on the link itself — the current-page treatment now keys off the attribute a screen reader already announces, so style and semantics cannot drift.- Three elevation modifier classes collapse into three
after:opacity-[var(--gtw19-g…)]utilities on the surfaces that own them, and the theme block re-points all three weights in one place. - The chart bars use static height utilities (
h-[32%]…h-full) instead of the CSS version's inline--grn-19-hproperty. Five heights, five utilities, no inline styles left. min-[62rem]:grid-cols-[16rem_minmax(0,1fr)]is the whole responsive shell — the app grid's breakpoint is now readable on the element rather than in a media block.- UI change from the CSS original: every nav link gains a
focus-visible:ring and the theme toggle forwards focus from its input. On a dark rail the inherited outline was the only indicator and it sat behind the grain layer.
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 Dark Mode Noise Without Muddy Shadows
Source: https://codefronts.com/design-styles/tailwind-css-grain-texture/tailwind-dark-mode-noise-without-muddy-shadows/
Grain gets ripped back out of dark products for one reason: overlay can only darken below mid-grey, so noise on a near-black surface turns elevation into mud. The fix is three parts — soft-light instead of overlay, roughly 1.6× the light-theme opacity, and a mask that keeps noise out of the deepest shadows. A dark dashboard shipping all three.
## HTML
```html
<section class="gtw-19 relative block w-full min-h-svh bg-gtw19-bg text-gtw19-ink" aria-labelledby="gtw-19-title">
<div class="absolute top-[clamp(1rem,3vw,1.75rem)] right-[clamp(0.85rem,3vw,1.75rem)] z-60">
<input type="checkbox" id="gtw-19-theme" class="gtw-19-theme peer sr-only">
<label for="gtw-19-theme" class="grid place-items-center w-11 h-11 rounded-full border border-gtw19-line bg-gtw19-s3 text-gtw19-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-gtw19-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 light theme</span>
</label>
</div>
<div class="gtw-19-app grid grid-cols-[minmax(0,1fr)] min-[62rem]:grid-cols-[16rem_minmax(0,1fr)] min-h-svh">
<aside aria-label="Sections" class="gtw-19-rail relative isolate overflow-hidden grid content-start gap-[1.2rem] p-[clamp(1.2rem,3vw,1.8rem)] border-r border-gtw19-line bg-gtw19-s1">
<p class="m-0 font-gtw19-display text-[1.05rem] font-semibold leading-none tracking-[-0.01em]">◐ Umbra</p>
<nav class="grid gap-[0.15rem]">
<a href="#gtw-19-title" aria-current="page" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink aria-[current=page]:bg-gtw19-s3 aria-[current=page]:text-gtw19-ink aria-[current=page]:shadow-[inset_2px_0_0_var(--color-gtw19-acc)] focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Overview</a>
<a href="#gtw-19-title" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Surfaces</a>
<a href="#gtw-19-title" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Elevation</a>
<a href="#gtw-19-title" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Contrast</a>
<a href="#gtw-19-title" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Tokens</a>
</nav>
<p class="m-0 pt-4 border-t border-gtw19-line text-gtw19-mut text-[0.8rem] text-pretty">Grain weight climbs with elevation. Shadows do almost nothing here.</p>
</aside>
<main class="grid content-start gap-[clamp(1.4rem,3.5vw,2.2rem)] p-[clamp(1.5rem,4vw,2.6rem)] pr-[clamp(4rem,8vw,5rem)]">
<header class="max-w-[46rem] grid gap-[0.55rem]">
<p class="m-0 font-gtw19-mono text-[0.72rem] font-medium leading-none tracking-[0.18em] uppercase text-gtw19-acc">Dark surface recipe · 19</p>
<h1 id="gtw-19-title" class="m-0 font-gtw19-display text-[clamp(1.8rem,4.6vw,2.9rem)] font-semibold leading-[1.05] tracking-[-0.035em] text-balance">Texture in the dark, without the mud</h1>
<p class="m-0 max-w-[40rem] text-gtw19-mut text-[clamp(0.93rem,1.8vw,1.04rem)] text-pretty">Two grain weights, <code class="font-gtw19-mono text-[0.9em]">soft-light</code> instead of <code class="font-gtw19-mono text-[0.9em]">overlay</code>, and a mask that keeps noise out of the deepest shadows. Toggle the theme and watch both numbers change.</p>
</header>
<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%,11rem),1fr))]">
<li class="relative isolate overflow-hidden grid gap-[0.15rem] min-w-0 px-[1.15rem] py-4 rounded-[14px] border border-gtw19-line bg-gtw19-s1 transition-[translate] duration-[260ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g1)]">
<span class="font-gtw19-mono text-[0.7rem] leading-none tracking-[0.12em] uppercase text-gtw19-mut">Surface 1</span>
<span class="font-gtw19-display text-[1.5rem] font-semibold leading-[1.1] tracking-[-0.03em]">8% L</span>
<span class="font-gtw19-mono text-[0.76rem] leading-none text-gtw19-acc">grain 0.055</span>
</li>
<li class="relative isolate overflow-hidden grid gap-[0.15rem] min-w-0 px-[1.15rem] py-4 rounded-[14px] border border-gtw19-line bg-gtw19-s2 transition-[translate] duration-[260ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g2)]">
<span class="font-gtw19-mono text-[0.7rem] leading-none tracking-[0.12em] uppercase text-gtw19-mut">Surface 2</span>
<span class="font-gtw19-display text-[1.5rem] font-semibold leading-[1.1] tracking-[-0.03em]">12% L</span>
<span class="font-gtw19-mono text-[0.76rem] leading-none text-gtw19-acc">grain 0.075</span>
</li>
<li class="relative isolate overflow-hidden grid gap-[0.15rem] min-w-0 px-[1.15rem] py-4 rounded-[14px] border border-gtw19-line bg-gtw19-s3 transition-[translate] duration-[260ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g3)]">
<span class="font-gtw19-mono text-[0.7rem] leading-none tracking-[0.12em] uppercase text-gtw19-mut">Surface 3</span>
<span class="font-gtw19-display text-[1.5rem] font-semibold leading-[1.1] tracking-[-0.03em]">17% L</span>
<span class="font-gtw19-mono text-[0.76rem] leading-none text-gtw19-acc">grain 0.095</span>
</li>
</ul>
<div class="grid gap-[clamp(0.8rem,2vw,1.2rem)] grid-cols-[repeat(auto-fit,minmax(min(100%,17rem),1fr))] items-start">
<section class="col-span-full relative isolate overflow-hidden grid content-start gap-[0.8rem] min-w-0 p-[clamp(1.1rem,2.6vw,1.5rem)] rounded-2xl border border-gtw19-line bg-gtw19-s2 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g2)]" aria-labelledby="gtw-19-h-chart">
<div class="flex items-center justify-between gap-[0.6rem] pb-[0.6rem] border-b border-gtw19-line">
<h2 id="gtw-19-h-chart" class="m-0 font-gtw19-display text-[1rem] font-semibold leading-tight tracking-[-0.015em]">Perceived texture vs opacity</h2>
<span class="px-2 py-[0.15rem] rounded-full bg-[color-mix(in_oklab,var(--color-gtw19-acc)_18%,transparent)] text-gtw19-acc font-gtw19-mono text-[0.66rem] font-medium leading-[1.6] tracking-[0.1em] uppercase">measured</span>
</div>
<div role="img" aria-label="Bar chart showing dark surfaces need roughly 1.6 times the grain opacity of light surfaces for equal perceived texture" class="grid grid-flow-col auto-cols-fr items-end gap-[clamp(0.4rem,1.5vw,0.9rem)] h-[clamp(7rem,16vw,10rem)] pt-2">
<span class="relative block h-[32%] rounded-t-lg bg-[color-mix(in_oklab,var(--color-gtw19-acc)_28%,var(--color-gtw19-s3))]"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-mut">0.03</b></span>
<span class="relative block h-[48%] rounded-t-lg bg-[color-mix(in_oklab,var(--color-gtw19-acc)_28%,var(--color-gtw19-s3))]"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-mut">0.045</b></span>
<span class="relative block h-[68%] rounded-t-lg bg-gtw19-acc"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-ink">0.06</b></span>
<span class="relative block h-[84%] rounded-t-lg bg-gtw19-acc"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-ink">0.075</b></span>
<span class="relative block h-full rounded-t-lg bg-[color-mix(in_oklab,var(--color-gtw19-acc)_28%,var(--color-gtw19-s3))]"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-mut">0.095</b></span>
</div>
<p class="m-0 text-gtw19-mut text-[0.83rem] text-pretty">The marked pair is the working range for dark product UI. The same perceived texture on a light surface sits at 0.04–0.05.</p>
</section>
<section class="relative isolate overflow-hidden grid content-start gap-[0.8rem] min-w-0 p-[clamp(1.1rem,2.6vw,1.5rem)] rounded-2xl border border-gtw19-line bg-gtw19-s2 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g2)]" aria-labelledby="gtw-19-h-blend">
<div class="flex items-center justify-between gap-[0.6rem] pb-[0.6rem] border-b border-gtw19-line"><h2 id="gtw-19-h-blend" class="m-0 font-gtw19-display text-[1rem] font-semibold leading-tight tracking-[-0.015em]">Blend choice</h2></div>
<ul class="list-none m-0 p-0 grid gap-[0.1rem]">
<li class="grid gap-[0.1rem] py-[0.55rem]"><b class="font-gtw19-mono text-[0.88rem] font-medium leading-tight">soft-light</b><span class="text-gtw19-mut text-[0.82rem] text-pretty">Ship this. Roughly symmetric at low lightness.</span></li>
<li class="grid gap-[0.1rem] py-[0.55rem] border-t border-gtw19-line"><b class="font-gtw19-mono text-[0.88rem] font-medium leading-tight">screen</b><span class="text-gtw19-mut text-[0.82rem] text-pretty">For true black. Can only lift, never crush.</span></li>
<li class="grid gap-[0.1rem] py-[0.55rem] border-t border-gtw19-line"><b class="font-gtw19-mono text-[0.88rem] font-medium leading-tight">overlay</b><span class="text-gtw19-mut text-[0.82rem] text-pretty">Avoid. Only darkens below mid-grey — this is the mud.</span></li>
</ul>
</section>
<section class="relative isolate overflow-hidden grid content-start gap-[0.8rem] min-w-0 p-[clamp(1.1rem,2.6vw,1.5rem)] rounded-2xl border border-gtw19-line bg-gtw19-s2 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g2)]" aria-labelledby="gtw-19-h-contrast">
<div class="flex items-center justify-between gap-[0.6rem] pb-[0.6rem] border-b border-gtw19-line"><h2 id="gtw-19-h-contrast" class="m-0 font-gtw19-display text-[1rem] font-semibold leading-tight tracking-[-0.015em]">Contrast, composited</h2></div>
<p class="m-0 text-gtw19-mut text-[0.83rem] text-pretty">A 9% grain layer measurably reduces text contrast. Check the composited surface, not the flat token.</p>
<dl class="grid gap-0 m-0">
<div class="flex items-baseline justify-between gap-[0.6rem] py-2 border-t border-gtw19-line"><dt class="m-0 text-gtw19-mut text-[0.83rem]">Body on surface 2</dt><dd class="m-0 font-gtw19-mono text-[0.9rem] font-semibold leading-none tabular-nums">7.1:1</dd></div>
<div class="flex items-baseline justify-between gap-[0.6rem] py-2 border-t border-gtw19-line"><dt class="m-0 text-gtw19-mut text-[0.83rem]">Same, grain applied</dt><dd class="m-0 font-gtw19-mono text-[0.9rem] font-semibold leading-none tabular-nums">6.8:1</dd></div>
<div class="flex items-baseline justify-between gap-[0.6rem] py-2 border-t border-gtw19-line"><dt class="m-0 text-gtw19-mut text-[0.83rem]">Muted on surface 2</dt><dd class="m-0 font-gtw19-mono text-[0.9rem] font-semibold leading-none tabular-nums">4.7:1</dd></div>
</dl>
</section>
</div>
</main>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-gtw19-bg: #08090b;
--color-gtw19-bg: oklch(0.14 0.005 265);
--color-gtw19-s1: #101216;
--color-gtw19-s1: oklch(0.19 0.008 265);
--color-gtw19-s2: #171a20;
--color-gtw19-s2: oklch(0.23 0.01 265);
--color-gtw19-s3: #20242c;
--color-gtw19-s3: oklch(0.28 0.012 265);
--color-gtw19-line: #262b34;
--color-gtw19-line: oklch(0.31 0.012 265);
--color-gtw19-ink: #eef1f6;
--color-gtw19-ink: oklch(0.95 0.008 265);
--color-gtw19-mut: #98a1b0;
--color-gtw19-mut: oklch(0.7 0.02 265);
--color-gtw19-acc: #7dd3fc;
--color-gtw19-acc: oklch(0.84 0.1 226);
--font-gtw19-display: 'Space Grotesk', 'Inter Tight', ui-sans-serif, system-ui, sans-serif;
--font-gtw19-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
.gtw-19 {
width: 100%;
min-height: 100svh;
--gtw19-g1: 0.055;
--gtw19-g2: 0.075;
--gtw19-g3: 0.095;
--gtw19-blend: soft-light;
--gtw19-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='gtw19n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23gtw19n)'/%3E%3C/svg%3E");
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.55;
}
.gtw-19-app {
min-height: 100vh;
min-height: 100svh;
}
/* The rail's grain fades out over the bottom 30%, where its shadow gradient lives — noise in
the deepest shadow is what kills elevation definition on dark UI. The -webkit- twin is
required for older Safari and has no utility form. */
.gtw-19-rail::after {
content: "";
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background-image: var(--gtw19-noise);
opacity: var(--gtw19-g1);
mix-blend-mode: var(--gtw19-blend);
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 70%, rgba(0, 0, 0, 0.35) 100%);
mask-image: linear-gradient(to bottom, #000 0%, #000 70%, rgba(0, 0, 0, 0.35) 100%);
}
.gtw-19:has(.gtw-19-theme:checked) {
--color-gtw19-bg: #f4f6fa;
--color-gtw19-bg: oklch(0.97 0.005 265);
--color-gtw19-s1: #ffffff;
--color-gtw19-s1: oklch(1 0 0);
--color-gtw19-s2: #f7f9fc;
--color-gtw19-s2: oklch(0.98 0.004 265);
--color-gtw19-s3: #eef2f8;
--color-gtw19-s3: oklch(0.95 0.008 265);
--color-gtw19-line: #dde3ec;
--color-gtw19-line: oklch(0.9 0.01 265);
--color-gtw19-ink: #0b0d11;
--color-gtw19-ink: oklch(0.16 0.008 265);
--color-gtw19-mut: #5b6474;
--color-gtw19-mut: oklch(0.47 0.02 265);
--color-gtw19-acc: #0369a1;
--color-gtw19-acc: oklch(0.48 0.11 235);
--gtw19-g1: 0.03;
--gtw19-g2: 0.045;
--gtw19-g3: 0.055;
--gtw19-blend: multiply;
}
@media (prefers-color-scheme: light) {
.gtw-19 {
--color-gtw19-bg: #f4f6fa;
--color-gtw19-bg: oklch(0.97 0.005 265);
--color-gtw19-s1: #ffffff;
--color-gtw19-s1: oklch(1 0 0);
--color-gtw19-s2: #f7f9fc;
--color-gtw19-s2: oklch(0.98 0.004 265);
--color-gtw19-s3: #eef2f8;
--color-gtw19-s3: oklch(0.95 0.008 265);
--color-gtw19-line: #dde3ec;
--color-gtw19-line: oklch(0.9 0.01 265);
--color-gtw19-ink: #0b0d11;
--color-gtw19-ink: oklch(0.16 0.008 265);
--color-gtw19-mut: #5b6474;
--color-gtw19-mut: oklch(0.47 0.02 265);
--color-gtw19-acc: #0369a1;
--color-gtw19-acc: oklch(0.48 0.11 235);
--gtw19-g1: 0.03;
--gtw19-g2: 0.045;
--gtw19-g3: 0.055;
--gtw19-blend: multiply;
}
.gtw-19:has(.gtw-19-theme:checked) {
--color-gtw19-bg: #08090b;
--color-gtw19-bg: oklch(0.14 0.005 265);
--color-gtw19-s1: #101216;
--color-gtw19-s1: oklch(0.19 0.008 265);
--color-gtw19-s2: #171a20;
--color-gtw19-s2: oklch(0.23 0.01 265);
--color-gtw19-s3: #20242c;
--color-gtw19-s3: oklch(0.28 0.012 265);
--color-gtw19-line: #262b34;
--color-gtw19-line: oklch(0.31 0.012 265);
--color-gtw19-ink: #eef1f6;
--color-gtw19-ink: oklch(0.95 0.008 265);
--color-gtw19-mut: #98a1b0;
--color-gtw19-mut: oklch(0.7 0.02 265);
--color-gtw19-acc: #7dd3fc;
--color-gtw19-acc: oklch(0.84 0.1 226);
--gtw19-g1: 0.055;
--gtw19-g2: 0.075;
--gtw19-g3: 0.095;
--gtw19-blend: soft-light;
}
}
@media (prefers-color-scheme: dark) {
.gtw-19:has(.gtw-19-theme:checked) {
--color-gtw19-bg: #f4f6fa;
--color-gtw19-bg: oklch(0.97 0.005 265);
--color-gtw19-s1: #ffffff;
--color-gtw19-s1: oklch(1 0 0);
--color-gtw19-s2: #f7f9fc;
--color-gtw19-s2: oklch(0.98 0.004 265);
--color-gtw19-s3: #eef2f8;
--color-gtw19-s3: oklch(0.95 0.008 265);
--color-gtw19-line: #dde3ec;
--color-gtw19-line: oklch(0.9 0.01 265);
--color-gtw19-ink: #0b0d11;
--color-gtw19-ink: oklch(0.16 0.008 265);
--color-gtw19-mut: #5b6474;
--color-gtw19-mut: oklch(0.47 0.02 265);
--color-gtw19-acc: #0369a1;
--color-gtw19-acc: oklch(0.48 0.11 235);
--gtw19-g1: 0.03;
--gtw19-g2: 0.045;
--gtw19-g3: 0.055;
--gtw19-blend: multiply;
}
}
```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 Dark Mode Noise Without Muddy Shadows
Source: https://codefronts.com/design-styles/tailwind-css-grain-texture/tailwind-dark-mode-noise-without-muddy-shadows/
Grain gets ripped back out of dark products for one reason: overlay can only darken below mid-grey, so noise on a near-black surface turns elevation into mud. The fix is three parts — soft-light instead of overlay, roughly 1.6× the light-theme opacity, and a mask that keeps noise out of the deepest shadows. A dark dashboard shipping all three.
## HTML
```html
<section class="gtw-19 relative block w-full min-h-svh bg-gtw19-bg text-gtw19-ink" aria-labelledby="gtw-19-title">
<div class="absolute top-[clamp(1rem,3vw,1.75rem)] right-[clamp(0.85rem,3vw,1.75rem)] z-60">
<input type="checkbox" id="gtw-19-theme" class="gtw-19-theme peer sr-only">
<label for="gtw-19-theme" class="grid place-items-center w-11 h-11 rounded-full border border-gtw19-line bg-gtw19-s3 text-gtw19-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-gtw19-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 light theme</span>
</label>
</div>
<div class="gtw-19-app grid grid-cols-[minmax(0,1fr)] min-[62rem]:grid-cols-[16rem_minmax(0,1fr)] min-h-svh">
<aside aria-label="Sections" class="gtw-19-rail relative isolate overflow-hidden grid content-start gap-[1.2rem] p-[clamp(1.2rem,3vw,1.8rem)] border-r border-gtw19-line bg-gtw19-s1">
<p class="m-0 font-gtw19-display text-[1.05rem] font-semibold leading-none tracking-[-0.01em]">◐ Umbra</p>
<nav class="grid gap-[0.15rem]">
<a href="#gtw-19-title" aria-current="page" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink aria-[current=page]:bg-gtw19-s3 aria-[current=page]:text-gtw19-ink aria-[current=page]:shadow-[inset_2px_0_0_var(--color-gtw19-acc)] focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Overview</a>
<a href="#gtw-19-title" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Surfaces</a>
<a href="#gtw-19-title" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Elevation</a>
<a href="#gtw-19-title" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Contrast</a>
<a href="#gtw-19-title" class="flex items-center min-h-11 px-3 rounded-[9px] text-gtw19-mut no-underline text-[0.92rem] transition-colors duration-150 hover:bg-gtw19-s2 hover:text-gtw19-ink focus-visible:outline-2 focus-visible:outline-gtw19-acc focus-visible:outline-offset-2">Tokens</a>
</nav>
<p class="m-0 pt-4 border-t border-gtw19-line text-gtw19-mut text-[0.8rem] text-pretty">Grain weight climbs with elevation. Shadows do almost nothing here.</p>
</aside>
<main class="grid content-start gap-[clamp(1.4rem,3.5vw,2.2rem)] p-[clamp(1.5rem,4vw,2.6rem)] pr-[clamp(4rem,8vw,5rem)]">
<header class="max-w-[46rem] grid gap-[0.55rem]">
<p class="m-0 font-gtw19-mono text-[0.72rem] font-medium leading-none tracking-[0.18em] uppercase text-gtw19-acc">Dark surface recipe · 19</p>
<h1 id="gtw-19-title" class="m-0 font-gtw19-display text-[clamp(1.8rem,4.6vw,2.9rem)] font-semibold leading-[1.05] tracking-[-0.035em] text-balance">Texture in the dark, without the mud</h1>
<p class="m-0 max-w-[40rem] text-gtw19-mut text-[clamp(0.93rem,1.8vw,1.04rem)] text-pretty">Two grain weights, <code class="font-gtw19-mono text-[0.9em]">soft-light</code> instead of <code class="font-gtw19-mono text-[0.9em]">overlay</code>, and a mask that keeps noise out of the deepest shadows. Toggle the theme and watch both numbers change.</p>
</header>
<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%,11rem),1fr))]">
<li class="relative isolate overflow-hidden grid gap-[0.15rem] min-w-0 px-[1.15rem] py-4 rounded-[14px] border border-gtw19-line bg-gtw19-s1 transition-[translate] duration-[260ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g1)]">
<span class="font-gtw19-mono text-[0.7rem] leading-none tracking-[0.12em] uppercase text-gtw19-mut">Surface 1</span>
<span class="font-gtw19-display text-[1.5rem] font-semibold leading-[1.1] tracking-[-0.03em]">8% L</span>
<span class="font-gtw19-mono text-[0.76rem] leading-none text-gtw19-acc">grain 0.055</span>
</li>
<li class="relative isolate overflow-hidden grid gap-[0.15rem] min-w-0 px-[1.15rem] py-4 rounded-[14px] border border-gtw19-line bg-gtw19-s2 transition-[translate] duration-[260ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g2)]">
<span class="font-gtw19-mono text-[0.7rem] leading-none tracking-[0.12em] uppercase text-gtw19-mut">Surface 2</span>
<span class="font-gtw19-display text-[1.5rem] font-semibold leading-[1.1] tracking-[-0.03em]">12% L</span>
<span class="font-gtw19-mono text-[0.76rem] leading-none text-gtw19-acc">grain 0.075</span>
</li>
<li class="relative isolate overflow-hidden grid gap-[0.15rem] min-w-0 px-[1.15rem] py-4 rounded-[14px] border border-gtw19-line bg-gtw19-s3 transition-[translate] duration-[260ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g3)]">
<span class="font-gtw19-mono text-[0.7rem] leading-none tracking-[0.12em] uppercase text-gtw19-mut">Surface 3</span>
<span class="font-gtw19-display text-[1.5rem] font-semibold leading-[1.1] tracking-[-0.03em]">17% L</span>
<span class="font-gtw19-mono text-[0.76rem] leading-none text-gtw19-acc">grain 0.095</span>
</li>
</ul>
<div class="grid gap-[clamp(0.8rem,2vw,1.2rem)] grid-cols-[repeat(auto-fit,minmax(min(100%,17rem),1fr))] items-start">
<section class="col-span-full relative isolate overflow-hidden grid content-start gap-[0.8rem] min-w-0 p-[clamp(1.1rem,2.6vw,1.5rem)] rounded-2xl border border-gtw19-line bg-gtw19-s2 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g2)]" aria-labelledby="gtw-19-h-chart">
<div class="flex items-center justify-between gap-[0.6rem] pb-[0.6rem] border-b border-gtw19-line">
<h2 id="gtw-19-h-chart" class="m-0 font-gtw19-display text-[1rem] font-semibold leading-tight tracking-[-0.015em]">Perceived texture vs opacity</h2>
<span class="px-2 py-[0.15rem] rounded-full bg-[color-mix(in_oklab,var(--color-gtw19-acc)_18%,transparent)] text-gtw19-acc font-gtw19-mono text-[0.66rem] font-medium leading-[1.6] tracking-[0.1em] uppercase">measured</span>
</div>
<div role="img" aria-label="Bar chart showing dark surfaces need roughly 1.6 times the grain opacity of light surfaces for equal perceived texture" class="grid grid-flow-col auto-cols-fr items-end gap-[clamp(0.4rem,1.5vw,0.9rem)] h-[clamp(7rem,16vw,10rem)] pt-2">
<span class="relative block h-[32%] rounded-t-lg bg-[color-mix(in_oklab,var(--color-gtw19-acc)_28%,var(--color-gtw19-s3))]"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-mut">0.03</b></span>
<span class="relative block h-[48%] rounded-t-lg bg-[color-mix(in_oklab,var(--color-gtw19-acc)_28%,var(--color-gtw19-s3))]"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-mut">0.045</b></span>
<span class="relative block h-[68%] rounded-t-lg bg-gtw19-acc"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-ink">0.06</b></span>
<span class="relative block h-[84%] rounded-t-lg bg-gtw19-acc"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-ink">0.075</b></span>
<span class="relative block h-full rounded-t-lg bg-[color-mix(in_oklab,var(--color-gtw19-acc)_28%,var(--color-gtw19-s3))]"><b class="absolute inset-x-0 -top-[1.35rem] text-center font-gtw19-mono text-[0.7rem] font-medium leading-none text-gtw19-mut">0.095</b></span>
</div>
<p class="m-0 text-gtw19-mut text-[0.83rem] text-pretty">The marked pair is the working range for dark product UI. The same perceived texture on a light surface sits at 0.04–0.05.</p>
</section>
<section class="relative isolate overflow-hidden grid content-start gap-[0.8rem] min-w-0 p-[clamp(1.1rem,2.6vw,1.5rem)] rounded-2xl border border-gtw19-line bg-gtw19-s2 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g2)]" aria-labelledby="gtw-19-h-blend">
<div class="flex items-center justify-between gap-[0.6rem] pb-[0.6rem] border-b border-gtw19-line"><h2 id="gtw-19-h-blend" class="m-0 font-gtw19-display text-[1rem] font-semibold leading-tight tracking-[-0.015em]">Blend choice</h2></div>
<ul class="list-none m-0 p-0 grid gap-[0.1rem]">
<li class="grid gap-[0.1rem] py-[0.55rem]"><b class="font-gtw19-mono text-[0.88rem] font-medium leading-tight">soft-light</b><span class="text-gtw19-mut text-[0.82rem] text-pretty">Ship this. Roughly symmetric at low lightness.</span></li>
<li class="grid gap-[0.1rem] py-[0.55rem] border-t border-gtw19-line"><b class="font-gtw19-mono text-[0.88rem] font-medium leading-tight">screen</b><span class="text-gtw19-mut text-[0.82rem] text-pretty">For true black. Can only lift, never crush.</span></li>
<li class="grid gap-[0.1rem] py-[0.55rem] border-t border-gtw19-line"><b class="font-gtw19-mono text-[0.88rem] font-medium leading-tight">overlay</b><span class="text-gtw19-mut text-[0.82rem] text-pretty">Avoid. Only darkens below mid-grey — this is the mud.</span></li>
</ul>
</section>
<section class="relative isolate overflow-hidden grid content-start gap-[0.8rem] min-w-0 p-[clamp(1.1rem,2.6vw,1.5rem)] rounded-2xl border border-gtw19-line bg-gtw19-s2 after:content-[''] after:absolute after:inset-0 after:-z-[1] after:pointer-events-none after:bg-[image:var(--gtw19-noise)] after:[mix-blend-mode:var(--gtw19-blend)] after:opacity-[var(--gtw19-g2)]" aria-labelledby="gtw-19-h-contrast">
<div class="flex items-center justify-between gap-[0.6rem] pb-[0.6rem] border-b border-gtw19-line"><h2 id="gtw-19-h-contrast" class="m-0 font-gtw19-display text-[1rem] font-semibold leading-tight tracking-[-0.015em]">Contrast, composited</h2></div>
<p class="m-0 text-gtw19-mut text-[0.83rem] text-pretty">A 9% grain layer measurably reduces text contrast. Check the composited surface, not the flat token.</p>
<dl class="grid gap-0 m-0">
<div class="flex items-baseline justify-between gap-[0.6rem] py-2 border-t border-gtw19-line"><dt class="m-0 text-gtw19-mut text-[0.83rem]">Body on surface 2</dt><dd class="m-0 font-gtw19-mono text-[0.9rem] font-semibold leading-none tabular-nums">7.1:1</dd></div>
<div class="flex items-baseline justify-between gap-[0.6rem] py-2 border-t border-gtw19-line"><dt class="m-0 text-gtw19-mut text-[0.83rem]">Same, grain applied</dt><dd class="m-0 font-gtw19-mono text-[0.9rem] font-semibold leading-none tabular-nums">6.8:1</dd></div>
<div class="flex items-baseline justify-between gap-[0.6rem] py-2 border-t border-gtw19-line"><dt class="m-0 text-gtw19-mut text-[0.83rem]">Muted on surface 2</dt><dd class="m-0 font-gtw19-mono text-[0.9rem] font-semibold leading-none tabular-nums">4.7:1</dd></div>
</dl>
</section>
</div>
</main>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-gtw19-bg: #08090b;
--color-gtw19-bg: oklch(0.14 0.005 265);
--color-gtw19-s1: #101216;
--color-gtw19-s1: oklch(0.19 0.008 265);
--color-gtw19-s2: #171a20;
--color-gtw19-s2: oklch(0.23 0.01 265);
--color-gtw19-s3: #20242c;
--color-gtw19-s3: oklch(0.28 0.012 265);
--color-gtw19-line: #262b34;
--color-gtw19-line: oklch(0.31 0.012 265);
--color-gtw19-ink: #eef1f6;
--color-gtw19-ink: oklch(0.95 0.008 265);
--color-gtw19-mut: #98a1b0;
--color-gtw19-mut: oklch(0.7 0.02 265);
--color-gtw19-acc: #7dd3fc;
--color-gtw19-acc: oklch(0.84 0.1 226);
--font-gtw19-display: 'Space Grotesk', 'Inter Tight', ui-sans-serif, system-ui, sans-serif;
--font-gtw19-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
.gtw-19 {
width: 100%;
min-height: 100svh;
--gtw19-g1: 0.055;
--gtw19-g2: 0.075;
--gtw19-g3: 0.095;
--gtw19-blend: soft-light;
--gtw19-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='gtw19n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23gtw19n)'/%3E%3C/svg%3E");
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.55;
}
.gtw-19-app {
min-height: 100vh;
min-height: 100svh;
}
/* The rail's grain fades out over the bottom 30%, where its shadow gradient lives — noise in
the deepest shadow is what kills elevation definition on dark UI. The -webkit- twin is
required for older Safari and has no utility form. */
.gtw-19-rail::after {
content: "";
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background-image: var(--gtw19-noise);
opacity: var(--gtw19-g1);
mix-blend-mode: var(--gtw19-blend);
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 70%, rgba(0, 0, 0, 0.35) 100%);
mask-image: linear-gradient(to bottom, #000 0%, #000 70%, rgba(0, 0, 0, 0.35) 100%);
}
.gtw-19:has(.gtw-19-theme:checked) {
--color-gtw19-bg: #f4f6fa;
--color-gtw19-bg: oklch(0.97 0.005 265);
--color-gtw19-s1: #ffffff;
--color-gtw19-s1: oklch(1 0 0);
--color-gtw19-s2: #f7f9fc;
--color-gtw19-s2: oklch(0.98 0.004 265);
--color-gtw19-s3: #eef2f8;
--color-gtw19-s3: oklch(0.95 0.008 265);
--color-gtw19-line: #dde3ec;
--color-gtw19-line: oklch(0.9 0.01 265);
--color-gtw19-ink: #0b0d11;
--color-gtw19-ink: oklch(0.16 0.008 265);
--color-gtw19-mut: #5b6474;
--color-gtw19-mut: oklch(0.47 0.02 265);
--color-gtw19-acc: #0369a1;
--color-gtw19-acc: oklch(0.48 0.11 235);
--gtw19-g1: 0.03;
--gtw19-g2: 0.045;
--gtw19-g3: 0.055;
--gtw19-blend: multiply;
}
@media (prefers-color-scheme: light) {
.gtw-19 {
--color-gtw19-bg: #f4f6fa;
--color-gtw19-bg: oklch(0.97 0.005 265);
--color-gtw19-s1: #ffffff;
--color-gtw19-s1: oklch(1 0 0);
--color-gtw19-s2: #f7f9fc;
--color-gtw19-s2: oklch(0.98 0.004 265);
--color-gtw19-s3: #eef2f8;
--color-gtw19-s3: oklch(0.95 0.008 265);
--color-gtw19-line: #dde3ec;
--color-gtw19-line: oklch(0.9 0.01 265);
--color-gtw19-ink: #0b0d11;
--color-gtw19-ink: oklch(0.16 0.008 265);
--color-gtw19-mut: #5b6474;
--color-gtw19-mut: oklch(0.47 0.02 265);
--color-gtw19-acc: #0369a1;
--color-gtw19-acc: oklch(0.48 0.11 235);
--gtw19-g1: 0.03;
--gtw19-g2: 0.045;
--gtw19-g3: 0.055;
--gtw19-blend: multiply;
}
.gtw-19:has(.gtw-19-theme:checked) {
--color-gtw19-bg: #08090b;
--color-gtw19-bg: oklch(0.14 0.005 265);
--color-gtw19-s1: #101216;
--color-gtw19-s1: oklch(0.19 0.008 265);
--color-gtw19-s2: #171a20;
--color-gtw19-s2: oklch(0.23 0.01 265);
--color-gtw19-s3: #20242c;
--color-gtw19-s3: oklch(0.28 0.012 265);
--color-gtw19-line: #262b34;
--color-gtw19-line: oklch(0.31 0.012 265);
--color-gtw19-ink: #eef1f6;
--color-gtw19-ink: oklch(0.95 0.008 265);
--color-gtw19-mut: #98a1b0;
--color-gtw19-mut: oklch(0.7 0.02 265);
--color-gtw19-acc: #7dd3fc;
--color-gtw19-acc: oklch(0.84 0.1 226);
--gtw19-g1: 0.055;
--gtw19-g2: 0.075;
--gtw19-g3: 0.095;
--gtw19-blend: soft-light;
}
}
@media (prefers-color-scheme: dark) {
.gtw-19:has(.gtw-19-theme:checked) {
--color-gtw19-bg: #f4f6fa;
--color-gtw19-bg: oklch(0.97 0.005 265);
--color-gtw19-s1: #ffffff;
--color-gtw19-s1: oklch(1 0 0);
--color-gtw19-s2: #f7f9fc;
--color-gtw19-s2: oklch(0.98 0.004 265);
--color-gtw19-s3: #eef2f8;
--color-gtw19-s3: oklch(0.95 0.008 265);
--color-gtw19-line: #dde3ec;
--color-gtw19-line: oklch(0.9 0.01 265);
--color-gtw19-ink: #0b0d11;
--color-gtw19-ink: oklch(0.16 0.008 265);
--color-gtw19-mut: #5b6474;
--color-gtw19-mut: oklch(0.47 0.02 265);
--color-gtw19-acc: #0369a1;
--color-gtw19-acc: oklch(0.48 0.11 235);
--gtw19-g1: 0.03;
--gtw19-g2: 0.045;
--gtw19-g3: 0.055;
--gtw19-blend: multiply;
}
}
```How this works
Why dark grain goes muddy. overlay multiplies below 50% lightness and screens above it. Every surface in a dark UI sits well below 50%, so overlay grain only ever darkens. Your 8% and 12% surfaces both get pushed toward black, the difference between them shrinks, and the elevation scale you carefully designed collapses into mud.
soft-light is roughly symmetric at low lightness — it lifts and lowers in proportion, so a textured surface keeps its place in the elevation ladder. On true black, screen is the other correct answer: it can only lift, never crush.
Dark surfaces need more grain, not less. Perceived texture at equal opacity is lower on dark surfaces because the absolute luminance difference the noise creates is smaller. The measured chart on this page is the argument: about 1.6× the light-theme value for the same felt roughness.
Shadows are the part everyone forgets. A drop shadow on near-black has almost no contrast to work with, so dark UI leans on surface lightness for elevation — and grain in the deepest corners kills what little shadow definition exists. The rail masks its noise out over the bottom 30%, which is where its shadow gradient lives.
And check contrast on the composited surface. A 9% grain layer measurably lowers text contrast: 7.1:1 becomes 6.8:1 in this panel. Auditing the flat token instead of the painted surface is how a grained dark theme fails an accessibility review after it ships.
Make it yours
- Blend:
--gtw19-blend.soft-lightfor dark,multiplyfor light,screenif your base is true#000. - Weights:
--gtw19-g1/g2/g3, matched to your surface lightness steps. Keep the ratio between them, not just the values. - Mask: the rail's
mask-imagestop (70% → 0.35 alpha). Push it higher if your shadows sit further up the surface. - Accent bar:
aria-[current=page]usesshadow-[inset_2px_0_0_…]. On dark UI an inset bar reads where a drop shadow does not. - Re-audit contrast after changing any weight. The ratios panel is there to remind you to measure the composited result.
Gotchas — read before shipping
- One grain opacity for both themes is always wrong for one of them. Ship two numbers.
overlayon a near-black surface can only darken, which crushes your elevation scale. This is the single most common dark-mode grain mistake.- Grain measurably reduces text contrast. Audit the composited surface, not the flat colour token — and do it after every weight change.
- Dark UI gets its elevation from surface lightness, not shadows. Adding grain without raising the lightness steps makes all three surfaces read the same.
mask-imageneeds the-webkit-twin for older Safari, which is why the rail's mask lives in CSS rather than in a utility.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
light-dark() would simplify the token block but needs Chrome 123, Safari 17.5 and Firefox 120, so this demo declares explicit values in both directions instead. mask-image needs the -webkit- prefix on older Safari, declared here. The quoted floor is Tailwind v4's generated sheet.