16 / 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 Paper Stock and Letterpress Press Effect
Letterpress in three parts: anisotropic fibre noise for the stock, a two-value text-shadow for the impression, and a deckle edge chewed by feDisplacementMap. One light source, declared once, shared by the type, the card edges and the paper itself — which is the only reason the effect is convincing.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="gtw-16 relative block w-full min-h-svh bg-gtw16-stock text-gtw16-ink" aria-labelledby="gtw-16-title">
<svg class="absolute w-0 h-0 overflow-hidden" width="0" height="0" aria-hidden="true" focusable="false">
<filter id="gtw-16-fibre" x="0" y="0" width="100%" height="100%" color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.04 0.7" numOctaves="4" seed="14" stitchTiles="stitch"/>
<feColorMatrix type="saturate" values="0"/>
</filter>
<filter id="gtw-16-deckle" x="-15%" y="-15%" width="130%" height="130%" color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.02 0.4" numOctaves="3" seed="7" result="gtw16n"/>
<feDisplacementMap in="SourceGraphic" in2="gtw16n" scale="9" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</svg>
<div class="absolute top-[clamp(1rem,3vw,1.75rem)] right-[clamp(0.85rem,3vw,1.75rem)] z-40">
<input type="checkbox" id="gtw-16-theme" class="gtw-16-theme peer sr-only">
<label for="gtw-16-theme" class="grid place-items-center w-11 h-11 rounded-full border border-gtw16-line bg-gtw16-stock2 text-gtw16-ink cursor-pointer shadow-[inset_0_1px_0_var(--gtw16-down)] peer-focus-visible:outline-2 peer-focus-visible:outline-gtw16-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>
<main class="relative z-[1] w-[min(72rem,calc(100%-clamp(1.5rem,5vw,4rem)))] mx-auto pt-[clamp(3rem,8vw,5rem)] pb-[clamp(2.5rem,6vw,4rem)] grid gap-[clamp(2rem,5vw,3rem)]">
<header class="max-w-[44rem] grid gap-2">
<p class="m-0 font-gtw16-mono text-[0.72rem] leading-none tracking-[0.18em] uppercase text-gtw16-mut">Stock No. 4 · 600gsm cotton rag</p>
<h1 id="gtw-16-title" class="m-0 font-gtw16-display text-[clamp(2.4rem,7vw,4.4rem)] font-normal leading-[0.98] tracking-[-0.02em] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down),0_0_1px_color-mix(in_oklab,var(--color-gtw16-ink)_18%,transparent)]">Pressed, not printed</h1>
<p class="m-0 max-w-[38rem] text-gtw16-mut text-[clamp(0.95rem,1.9vw,1.06rem)] text-pretty">Directional fibre for the stock, a two-value <code class="font-gtw16-mono text-[0.9em]">text-shadow</code> for the press, and one light source shared by every layer.</p>
</header>
<article class="relative p-[clamp(0.5rem,2vw,1rem)]">
<div class="relative grid justify-items-center gap-[0.7rem] text-center px-[clamp(1.4rem,5vw,3.5rem)] py-[clamp(2.2rem,6vw,4rem)] bg-gtw16-stock2 shadow-[inset_0_1px_0_var(--gtw16-down),0_22px_50px_-30px_oklch(0.3_0.03_70/0.45)] [filter:url(#gtw-16-deckle)]">
<p aria-hidden="true" class="m-0 text-[1.1rem] text-gtw16-acc">✦</p>
<p class="m-0 font-gtw16-display text-[clamp(1.6rem,5vw,2.8rem)] font-normal leading-[1.1] tracking-[0.01em] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down)]">Aleksandra & Tomás</p>
<h2 class="m-0 max-w-[26rem] text-[clamp(0.95rem,2.2vw,1.15rem)] font-normal leading-[1.5] tracking-[0.06em] lowercase text-gtw16-mut">request the pleasure of your company</h2>
<p class="mt-[0.3rem] mb-0 font-gtw16-display text-[clamp(1.05rem,2.6vw,1.35rem)] font-normal leading-[1.5] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down)]">Saturday, the fourteenth of June<br>Two thousand and twenty-seven</p>
<div aria-hidden="true" class="w-12 h-px bg-gtw16-acc"></div>
<p class="m-0 font-gtw16-mono text-[0.78rem] leading-[1.6] tracking-[0.14em] uppercase text-gtw16-mut">Bracken Hall · Northumberland · 4 o'clock</p>
</div>
</article>
<div class="grid gap-[clamp(1.2rem,3vw,2rem)] grid-cols-[repeat(auto-fit,minmax(min(100%,18rem),1fr))] items-start">
<article class="grid content-center gap-[0.4rem] aspect-[85/55] p-[clamp(1.1rem,3vw,1.6rem)] rounded-sm bg-gtw16-stock2 shadow-[inset_0_1px_0_var(--gtw16-down),0_12px_30px_-20px_oklch(0.3_0.03_70/0.4)] transition-[translate,rotate,box-shadow] duration-[380ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-[3px] hover:-rotate-[0.4deg] motion-reduce:transition-none motion-reduce:hover:translate-y-0 motion-reduce:hover:rotate-0">
<p class="m-0 font-gtw16-display text-[clamp(1.3rem,3.4vw,1.8rem)] font-normal leading-[1.1] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down)]">Rowan Ellis</p>
<p class="m-0 font-gtw16-mono text-[0.72rem] leading-[1.4] tracking-[0.14em] uppercase text-gtw16-acc">Compositor & Press Operator</p>
<dl class="grid gap-[0.1rem] mt-2 mb-0 pt-[0.6rem] border-t border-gtw16-line">
<div class="flex gap-2"><dt class="m-0 min-w-[3.4rem] font-gtw16-mono text-[0.7rem] leading-[1.6] tracking-[0.1em] uppercase text-gtw16-mut">Studio</dt><dd class="m-0 text-[0.85rem]">Unit 7, Sackville Yard</dd></div>
<div class="flex gap-2"><dt class="m-0 min-w-[3.4rem] font-gtw16-mono text-[0.7rem] leading-[1.6] tracking-[0.1em] uppercase text-gtw16-mut">Post</dt><dd class="m-0 text-[0.85rem]">rowan@bracken.press</dd></div>
</dl>
</article>
<section class="grid gap-[0.7rem] p-[clamp(1.1rem,3vw,1.7rem)] rounded-sm border border-gtw16-line bg-[color-mix(in_oklab,var(--color-gtw16-stock2)_60%,transparent)]" aria-labelledby="gtw-16-h-spec">
<h2 id="gtw-16-h-spec" class="m-0 font-gtw16-display text-[clamp(1.2rem,3vw,1.6rem)] font-normal leading-[1.15] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down)]">Press specification</h2>
<dl class="grid gap-0 m-0">
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Stock</dt><dd class="m-0 text-[0.88rem]">600gsm cotton rag, deckle edge</dd></div>
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Fibre</dt><dd class="m-0 text-[0.88rem]">anisotropic turbulence · 0.04 0.7</dd></div>
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Impression</dt><dd class="m-0 text-[0.88rem]">text-shadow · 1px above, 1px below</dd></div>
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Ink bleed</dt><dd class="m-0 text-[0.88rem]">0 0 1px at 18% ink</dd></div>
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Light</dt><dd class="m-0 text-[0.88rem]">above, consistent across all layers</dd></div>
</dl>
<p class="m-0 text-gtw16-mut text-[0.83rem] text-pretty">Deboss below 13px hurts legibility. Cap the impression at your body size and drop it entirely for captions.</p>
</section>
</div>
</main>
</section><section class="gtw-16 relative block w-full min-h-svh bg-gtw16-stock text-gtw16-ink" aria-labelledby="gtw-16-title">
<svg class="absolute w-0 h-0 overflow-hidden" width="0" height="0" aria-hidden="true" focusable="false">
<filter id="gtw-16-fibre" x="0" y="0" width="100%" height="100%" color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.04 0.7" numOctaves="4" seed="14" stitchTiles="stitch"/>
<feColorMatrix type="saturate" values="0"/>
</filter>
<filter id="gtw-16-deckle" x="-15%" y="-15%" width="130%" height="130%" color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.02 0.4" numOctaves="3" seed="7" result="gtw16n"/>
<feDisplacementMap in="SourceGraphic" in2="gtw16n" scale="9" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</svg>
<div class="absolute top-[clamp(1rem,3vw,1.75rem)] right-[clamp(0.85rem,3vw,1.75rem)] z-40">
<input type="checkbox" id="gtw-16-theme" class="gtw-16-theme peer sr-only">
<label for="gtw-16-theme" class="grid place-items-center w-11 h-11 rounded-full border border-gtw16-line bg-gtw16-stock2 text-gtw16-ink cursor-pointer shadow-[inset_0_1px_0_var(--gtw16-down)] peer-focus-visible:outline-2 peer-focus-visible:outline-gtw16-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>
<main class="relative z-[1] w-[min(72rem,calc(100%-clamp(1.5rem,5vw,4rem)))] mx-auto pt-[clamp(3rem,8vw,5rem)] pb-[clamp(2.5rem,6vw,4rem)] grid gap-[clamp(2rem,5vw,3rem)]">
<header class="max-w-[44rem] grid gap-2">
<p class="m-0 font-gtw16-mono text-[0.72rem] leading-none tracking-[0.18em] uppercase text-gtw16-mut">Stock No. 4 · 600gsm cotton rag</p>
<h1 id="gtw-16-title" class="m-0 font-gtw16-display text-[clamp(2.4rem,7vw,4.4rem)] font-normal leading-[0.98] tracking-[-0.02em] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down),0_0_1px_color-mix(in_oklab,var(--color-gtw16-ink)_18%,transparent)]">Pressed, not printed</h1>
<p class="m-0 max-w-[38rem] text-gtw16-mut text-[clamp(0.95rem,1.9vw,1.06rem)] text-pretty">Directional fibre for the stock, a two-value <code class="font-gtw16-mono text-[0.9em]">text-shadow</code> for the press, and one light source shared by every layer.</p>
</header>
<article class="relative p-[clamp(0.5rem,2vw,1rem)]">
<div class="relative grid justify-items-center gap-[0.7rem] text-center px-[clamp(1.4rem,5vw,3.5rem)] py-[clamp(2.2rem,6vw,4rem)] bg-gtw16-stock2 shadow-[inset_0_1px_0_var(--gtw16-down),0_22px_50px_-30px_oklch(0.3_0.03_70/0.45)] [filter:url(#gtw-16-deckle)]">
<p aria-hidden="true" class="m-0 text-[1.1rem] text-gtw16-acc">✦</p>
<p class="m-0 font-gtw16-display text-[clamp(1.6rem,5vw,2.8rem)] font-normal leading-[1.1] tracking-[0.01em] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down)]">Aleksandra & Tomás</p>
<h2 class="m-0 max-w-[26rem] text-[clamp(0.95rem,2.2vw,1.15rem)] font-normal leading-[1.5] tracking-[0.06em] lowercase text-gtw16-mut">request the pleasure of your company</h2>
<p class="mt-[0.3rem] mb-0 font-gtw16-display text-[clamp(1.05rem,2.6vw,1.35rem)] font-normal leading-[1.5] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down)]">Saturday, the fourteenth of June<br>Two thousand and twenty-seven</p>
<div aria-hidden="true" class="w-12 h-px bg-gtw16-acc"></div>
<p class="m-0 font-gtw16-mono text-[0.78rem] leading-[1.6] tracking-[0.14em] uppercase text-gtw16-mut">Bracken Hall · Northumberland · 4 o'clock</p>
</div>
</article>
<div class="grid gap-[clamp(1.2rem,3vw,2rem)] grid-cols-[repeat(auto-fit,minmax(min(100%,18rem),1fr))] items-start">
<article class="grid content-center gap-[0.4rem] aspect-[85/55] p-[clamp(1.1rem,3vw,1.6rem)] rounded-sm bg-gtw16-stock2 shadow-[inset_0_1px_0_var(--gtw16-down),0_12px_30px_-20px_oklch(0.3_0.03_70/0.4)] transition-[translate,rotate,box-shadow] duration-[380ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-[3px] hover:-rotate-[0.4deg] motion-reduce:transition-none motion-reduce:hover:translate-y-0 motion-reduce:hover:rotate-0">
<p class="m-0 font-gtw16-display text-[clamp(1.3rem,3.4vw,1.8rem)] font-normal leading-[1.1] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down)]">Rowan Ellis</p>
<p class="m-0 font-gtw16-mono text-[0.72rem] leading-[1.4] tracking-[0.14em] uppercase text-gtw16-acc">Compositor & Press Operator</p>
<dl class="grid gap-[0.1rem] mt-2 mb-0 pt-[0.6rem] border-t border-gtw16-line">
<div class="flex gap-2"><dt class="m-0 min-w-[3.4rem] font-gtw16-mono text-[0.7rem] leading-[1.6] tracking-[0.1em] uppercase text-gtw16-mut">Studio</dt><dd class="m-0 text-[0.85rem]">Unit 7, Sackville Yard</dd></div>
<div class="flex gap-2"><dt class="m-0 min-w-[3.4rem] font-gtw16-mono text-[0.7rem] leading-[1.6] tracking-[0.1em] uppercase text-gtw16-mut">Post</dt><dd class="m-0 text-[0.85rem]">rowan@bracken.press</dd></div>
</dl>
</article>
<section class="grid gap-[0.7rem] p-[clamp(1.1rem,3vw,1.7rem)] rounded-sm border border-gtw16-line bg-[color-mix(in_oklab,var(--color-gtw16-stock2)_60%,transparent)]" aria-labelledby="gtw-16-h-spec">
<h2 id="gtw-16-h-spec" class="m-0 font-gtw16-display text-[clamp(1.2rem,3vw,1.6rem)] font-normal leading-[1.15] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down)]">Press specification</h2>
<dl class="grid gap-0 m-0">
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Stock</dt><dd class="m-0 text-[0.88rem]">600gsm cotton rag, deckle edge</dd></div>
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Fibre</dt><dd class="m-0 text-[0.88rem]">anisotropic turbulence · 0.04 0.7</dd></div>
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Impression</dt><dd class="m-0 text-[0.88rem]">text-shadow · 1px above, 1px below</dd></div>
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Ink bleed</dt><dd class="m-0 text-[0.88rem]">0 0 1px at 18% ink</dd></div>
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Light</dt><dd class="m-0 text-[0.88rem]">above, consistent across all layers</dd></div>
</dl>
<p class="m-0 text-gtw16-mut text-[0.83rem] text-pretty">Deboss below 13px hurts legibility. Cap the impression at your body size and drop it entirely for captions.</p>
</section>
</div>
</main>
</section>@import "tailwindcss";
@theme {
--color-gtw16-stock: #efe9dc;
--color-gtw16-stock: oklch(0.92 0.02 84);
--color-gtw16-stock2: #f8f4ea;
--color-gtw16-stock2: oklch(0.96 0.015 85);
--color-gtw16-ink: #221f1a;
--color-gtw16-ink: oklch(0.24 0.01 70);
--color-gtw16-mut: #6b6355;
--color-gtw16-mut: oklch(0.47 0.015 80);
--color-gtw16-line: #d8cfbc;
--color-gtw16-line: oklch(0.86 0.02 84);
--color-gtw16-acc: #8c2f39;
--color-gtw16-acc: oklch(0.42 0.13 20);
--font-gtw16-display: 'Instrument Serif', 'Cormorant Garamond', 'Playfair Display', Georgia, serif;
--font-gtw16-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
.gtw-16 {
width: 100%;
min-height: 100svh;
--gtw16-press: 1px;
--gtw16-fibre: 0.06;
--gtw16-up: rgba(34, 31, 26, 0.24);
--gtw16-down: rgba(255, 255, 255, 0.92);
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.6;
}
/* The paper itself: a fixed, viewport-covering fibre layer. It is a ::before on the root
rather than a utility because it must sit behind every child at z-index 0 and never scroll. */
.gtw-16::before {
content: "";
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
filter: url(#gtw-16-fibre);
opacity: var(--gtw16-fibre);
mix-blend-mode: multiply;
}
.gtw-16:has(.gtw-16-theme:checked) {
--color-gtw16-stock: #171512;
--color-gtw16-stock: oklch(0.19 0.01 70);
--color-gtw16-stock2: #201d19;
--color-gtw16-stock2: oklch(0.24 0.012 70);
--color-gtw16-ink: #f0e9da;
--color-gtw16-ink: oklch(0.93 0.02 82);
--color-gtw16-mut: #a49a86;
--color-gtw16-mut: oklch(0.7 0.02 82);
--color-gtw16-line: #322c24;
--color-gtw16-line: oklch(0.31 0.015 78);
--color-gtw16-acc: #d9a04a;
--color-gtw16-acc: oklch(0.75 0.12 74);
--gtw16-up: rgba(0, 0, 0, 0.6);
--gtw16-down: rgba(255, 255, 255, 0.14);
--gtw16-fibre: 0.09;
}
@media (prefers-color-scheme: dark) {
.gtw-16 {
--color-gtw16-stock: #171512;
--color-gtw16-stock: oklch(0.19 0.01 70);
--color-gtw16-stock2: #201d19;
--color-gtw16-stock2: oklch(0.24 0.012 70);
--color-gtw16-ink: #f0e9da;
--color-gtw16-ink: oklch(0.93 0.02 82);
--color-gtw16-mut: #a49a86;
--color-gtw16-mut: oklch(0.7 0.02 82);
--color-gtw16-line: #322c24;
--color-gtw16-line: oklch(0.31 0.015 78);
--color-gtw16-acc: #d9a04a;
--color-gtw16-acc: oklch(0.75 0.12 74);
--gtw16-up: rgba(0, 0, 0, 0.6);
--gtw16-down: rgba(255, 255, 255, 0.14);
--gtw16-fibre: 0.09;
}
.gtw-16:has(.gtw-16-theme:checked) {
--color-gtw16-stock: #efe9dc;
--color-gtw16-stock: oklch(0.92 0.02 84);
--color-gtw16-stock2: #f8f4ea;
--color-gtw16-stock2: oklch(0.96 0.015 85);
--color-gtw16-ink: #221f1a;
--color-gtw16-ink: oklch(0.24 0.01 70);
--color-gtw16-mut: #6b6355;
--color-gtw16-mut: oklch(0.47 0.015 80);
--color-gtw16-line: #d8cfbc;
--color-gtw16-line: oklch(0.86 0.02 84);
--color-gtw16-acc: #8c2f39;
--color-gtw16-acc: oklch(0.42 0.13 20);
--gtw16-up: rgba(34, 31, 26, 0.24);
--gtw16-down: rgba(255, 255, 255, 0.92);
--gtw16-fibre: 0.06;
}
}
@media (prefers-color-scheme: light) {
.gtw-16:has(.gtw-16-theme:checked) {
--color-gtw16-stock: #171512;
--color-gtw16-stock: oklch(0.19 0.01 70);
--color-gtw16-stock2: #201d19;
--color-gtw16-stock2: oklch(0.24 0.012 70);
--color-gtw16-ink: #f0e9da;
--color-gtw16-ink: oklch(0.93 0.02 82);
--color-gtw16-mut: #a49a86;
--color-gtw16-mut: oklch(0.7 0.02 82);
--color-gtw16-line: #322c24;
--color-gtw16-line: oklch(0.31 0.015 78);
--color-gtw16-acc: #d9a04a;
--color-gtw16-acc: oklch(0.75 0.12 74);
--gtw16-up: rgba(0, 0, 0, 0.6);
--gtw16-down: rgba(255, 255, 255, 0.14);
--gtw16-fibre: 0.09;
}
}@import "tailwindcss";
@theme {
--color-gtw16-stock: #efe9dc;
--color-gtw16-stock: oklch(0.92 0.02 84);
--color-gtw16-stock2: #f8f4ea;
--color-gtw16-stock2: oklch(0.96 0.015 85);
--color-gtw16-ink: #221f1a;
--color-gtw16-ink: oklch(0.24 0.01 70);
--color-gtw16-mut: #6b6355;
--color-gtw16-mut: oklch(0.47 0.015 80);
--color-gtw16-line: #d8cfbc;
--color-gtw16-line: oklch(0.86 0.02 84);
--color-gtw16-acc: #8c2f39;
--color-gtw16-acc: oklch(0.42 0.13 20);
--font-gtw16-display: 'Instrument Serif', 'Cormorant Garamond', 'Playfair Display', Georgia, serif;
--font-gtw16-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
.gtw-16 {
width: 100%;
min-height: 100svh;
--gtw16-press: 1px;
--gtw16-fibre: 0.06;
--gtw16-up: rgba(34, 31, 26, 0.24);
--gtw16-down: rgba(255, 255, 255, 0.92);
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.6;
}
/* The paper itself: a fixed, viewport-covering fibre layer. It is a ::before on the root
rather than a utility because it must sit behind every child at z-index 0 and never scroll. */
.gtw-16::before {
content: "";
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
filter: url(#gtw-16-fibre);
opacity: var(--gtw16-fibre);
mix-blend-mode: multiply;
}
.gtw-16:has(.gtw-16-theme:checked) {
--color-gtw16-stock: #171512;
--color-gtw16-stock: oklch(0.19 0.01 70);
--color-gtw16-stock2: #201d19;
--color-gtw16-stock2: oklch(0.24 0.012 70);
--color-gtw16-ink: #f0e9da;
--color-gtw16-ink: oklch(0.93 0.02 82);
--color-gtw16-mut: #a49a86;
--color-gtw16-mut: oklch(0.7 0.02 82);
--color-gtw16-line: #322c24;
--color-gtw16-line: oklch(0.31 0.015 78);
--color-gtw16-acc: #d9a04a;
--color-gtw16-acc: oklch(0.75 0.12 74);
--gtw16-up: rgba(0, 0, 0, 0.6);
--gtw16-down: rgba(255, 255, 255, 0.14);
--gtw16-fibre: 0.09;
}
@media (prefers-color-scheme: dark) {
.gtw-16 {
--color-gtw16-stock: #171512;
--color-gtw16-stock: oklch(0.19 0.01 70);
--color-gtw16-stock2: #201d19;
--color-gtw16-stock2: oklch(0.24 0.012 70);
--color-gtw16-ink: #f0e9da;
--color-gtw16-ink: oklch(0.93 0.02 82);
--color-gtw16-mut: #a49a86;
--color-gtw16-mut: oklch(0.7 0.02 82);
--color-gtw16-line: #322c24;
--color-gtw16-line: oklch(0.31 0.015 78);
--color-gtw16-acc: #d9a04a;
--color-gtw16-acc: oklch(0.75 0.12 74);
--gtw16-up: rgba(0, 0, 0, 0.6);
--gtw16-down: rgba(255, 255, 255, 0.14);
--gtw16-fibre: 0.09;
}
.gtw-16:has(.gtw-16-theme:checked) {
--color-gtw16-stock: #efe9dc;
--color-gtw16-stock: oklch(0.92 0.02 84);
--color-gtw16-stock2: #f8f4ea;
--color-gtw16-stock2: oklch(0.96 0.015 85);
--color-gtw16-ink: #221f1a;
--color-gtw16-ink: oklch(0.24 0.01 70);
--color-gtw16-mut: #6b6355;
--color-gtw16-mut: oklch(0.47 0.015 80);
--color-gtw16-line: #d8cfbc;
--color-gtw16-line: oklch(0.86 0.02 84);
--color-gtw16-acc: #8c2f39;
--color-gtw16-acc: oklch(0.42 0.13 20);
--gtw16-up: rgba(34, 31, 26, 0.24);
--gtw16-down: rgba(255, 255, 255, 0.92);
--gtw16-fibre: 0.06;
}
}
@media (prefers-color-scheme: light) {
.gtw-16:has(.gtw-16-theme:checked) {
--color-gtw16-stock: #171512;
--color-gtw16-stock: oklch(0.19 0.01 70);
--color-gtw16-stock2: #201d19;
--color-gtw16-stock2: oklch(0.24 0.012 70);
--color-gtw16-ink: #f0e9da;
--color-gtw16-ink: oklch(0.93 0.02 82);
--color-gtw16-mut: #a49a86;
--color-gtw16-mut: oklch(0.7 0.02 82);
--color-gtw16-line: #322c24;
--color-gtw16-line: oklch(0.31 0.015 78);
--color-gtw16-acc: #d9a04a;
--color-gtw16-acc: oklch(0.75 0.12 74);
--gtw16-up: rgba(0, 0, 0, 0.6);
--gtw16-down: rgba(255, 255, 255, 0.14);
--gtw16-fibre: 0.09;
}
}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-gtw16-stock: #efe9dc;
--color-gtw16-stock: oklch(0.92 0.02 84);
--color-gtw16-stock2: #f8f4ea;
--color-gtw16-stock2: oklch(0.96 0.015 85);
--color-gtw16-ink: #221f1a;
--color-gtw16-ink: oklch(0.24 0.01 70);
--color-gtw16-mut: #6b6355;
--color-gtw16-mut: oklch(0.47 0.015 80);
--color-gtw16-line: #d8cfbc;
--color-gtw16-line: oklch(0.86 0.02 84);
--color-gtw16-acc: #8c2f39;
--color-gtw16-acc: oklch(0.42 0.13 20);
--font-gtw16-display: 'Instrument Serif', 'Cormorant Garamond', 'Playfair Display', Georgia, serif;
--font-gtw16-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
.gtw-16 {
width: 100%;
min-height: 100svh;
--gtw16-press: 1px;
--gtw16-fibre: 0.06;
--gtw16-up: rgba(34, 31, 26, 0.24);
--gtw16-down: rgba(255, 255, 255, 0.92);
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.6;
}
/* The paper itself: a fixed, viewport-covering fibre layer. It is a ::before on the root
rather than a utility because it must sit behind every child at z-index 0 and never scroll. */
.gtw-16::before {
content: "";
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
filter: url(#gtw-16-fibre);
opacity: var(--gtw16-fibre);
mix-blend-mode: multiply;
}
.gtw-16:has(.gtw-16-theme:checked) {
--color-gtw16-stock: #171512;
--color-gtw16-stock: oklch(0.19 0.01 70);
--color-gtw16-stock2: #201d19;
--color-gtw16-stock2: oklch(0.24 0.012 70);
--color-gtw16-ink: #f0e9da;
--color-gtw16-ink: oklch(0.93 0.02 82);
--color-gtw16-mut: #a49a86;
--color-gtw16-mut: oklch(0.7 0.02 82);
--color-gtw16-line: #322c24;
--color-gtw16-line: oklch(0.31 0.015 78);
--color-gtw16-acc: #d9a04a;
--color-gtw16-acc: oklch(0.75 0.12 74);
--gtw16-up: rgba(0, 0, 0, 0.6);
--gtw16-down: rgba(255, 255, 255, 0.14);
--gtw16-fibre: 0.09;
}
@media (prefers-color-scheme: dark) {
.gtw-16 {
--color-gtw16-stock: #171512;
--color-gtw16-stock: oklch(0.19 0.01 70);
--color-gtw16-stock2: #201d19;
--color-gtw16-stock2: oklch(0.24 0.012 70);
--color-gtw16-ink: #f0e9da;
--color-gtw16-ink: oklch(0.93 0.02 82);
--color-gtw16-mut: #a49a86;
--color-gtw16-mut: oklch(0.7 0.02 82);
--color-gtw16-line: #322c24;
--color-gtw16-line: oklch(0.31 0.015 78);
--color-gtw16-acc: #d9a04a;
--color-gtw16-acc: oklch(0.75 0.12 74);
--gtw16-up: rgba(0, 0, 0, 0.6);
--gtw16-down: rgba(255, 255, 255, 0.14);
--gtw16-fibre: 0.09;
}
.gtw-16:has(.gtw-16-theme:checked) {
--color-gtw16-stock: #efe9dc;
--color-gtw16-stock: oklch(0.92 0.02 84);
--color-gtw16-stock2: #f8f4ea;
--color-gtw16-stock2: oklch(0.96 0.015 85);
--color-gtw16-ink: #221f1a;
--color-gtw16-ink: oklch(0.24 0.01 70);
--color-gtw16-mut: #6b6355;
--color-gtw16-mut: oklch(0.47 0.015 80);
--color-gtw16-line: #d8cfbc;
--color-gtw16-line: oklch(0.86 0.02 84);
--color-gtw16-acc: #8c2f39;
--color-gtw16-acc: oklch(0.42 0.13 20);
--gtw16-up: rgba(34, 31, 26, 0.24);
--gtw16-down: rgba(255, 255, 255, 0.92);
--gtw16-fibre: 0.06;
}
}
@media (prefers-color-scheme: light) {
.gtw-16:has(.gtw-16-theme:checked) {
--color-gtw16-stock: #171512;
--color-gtw16-stock: oklch(0.19 0.01 70);
--color-gtw16-stock2: #201d19;
--color-gtw16-stock2: oklch(0.24 0.012 70);
--color-gtw16-ink: #f0e9da;
--color-gtw16-ink: oklch(0.93 0.02 82);
--color-gtw16-mut: #a49a86;
--color-gtw16-mut: oklch(0.7 0.02 82);
--color-gtw16-line: #322c24;
--color-gtw16-line: oklch(0.31 0.015 78);
--color-gtw16-acc: #d9a04a;
--color-gtw16-acc: oklch(0.75 0.12 74);
--gtw16-up: rgba(0, 0, 0, 0.6);
--gtw16-down: rgba(255, 255, 255, 0.14);
--gtw16-fibre: 0.09;
}
}@import "tailwindcss";
@theme {
--color-gtw16-stock: #efe9dc;
--color-gtw16-stock: oklch(0.92 0.02 84);
--color-gtw16-stock2: #f8f4ea;
--color-gtw16-stock2: oklch(0.96 0.015 85);
--color-gtw16-ink: #221f1a;
--color-gtw16-ink: oklch(0.24 0.01 70);
--color-gtw16-mut: #6b6355;
--color-gtw16-mut: oklch(0.47 0.015 80);
--color-gtw16-line: #d8cfbc;
--color-gtw16-line: oklch(0.86 0.02 84);
--color-gtw16-acc: #8c2f39;
--color-gtw16-acc: oklch(0.42 0.13 20);
--font-gtw16-display: 'Instrument Serif', 'Cormorant Garamond', 'Playfair Display', Georgia, serif;
--font-gtw16-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
.gtw-16 {
width: 100%;
min-height: 100svh;
--gtw16-press: 1px;
--gtw16-fibre: 0.06;
--gtw16-up: rgba(34, 31, 26, 0.24);
--gtw16-down: rgba(255, 255, 255, 0.92);
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.6;
}
/* The paper itself: a fixed, viewport-covering fibre layer. It is a ::before on the root
rather than a utility because it must sit behind every child at z-index 0 and never scroll. */
.gtw-16::before {
content: "";
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
filter: url(#gtw-16-fibre);
opacity: var(--gtw16-fibre);
mix-blend-mode: multiply;
}
.gtw-16:has(.gtw-16-theme:checked) {
--color-gtw16-stock: #171512;
--color-gtw16-stock: oklch(0.19 0.01 70);
--color-gtw16-stock2: #201d19;
--color-gtw16-stock2: oklch(0.24 0.012 70);
--color-gtw16-ink: #f0e9da;
--color-gtw16-ink: oklch(0.93 0.02 82);
--color-gtw16-mut: #a49a86;
--color-gtw16-mut: oklch(0.7 0.02 82);
--color-gtw16-line: #322c24;
--color-gtw16-line: oklch(0.31 0.015 78);
--color-gtw16-acc: #d9a04a;
--color-gtw16-acc: oklch(0.75 0.12 74);
--gtw16-up: rgba(0, 0, 0, 0.6);
--gtw16-down: rgba(255, 255, 255, 0.14);
--gtw16-fibre: 0.09;
}
@media (prefers-color-scheme: dark) {
.gtw-16 {
--color-gtw16-stock: #171512;
--color-gtw16-stock: oklch(0.19 0.01 70);
--color-gtw16-stock2: #201d19;
--color-gtw16-stock2: oklch(0.24 0.012 70);
--color-gtw16-ink: #f0e9da;
--color-gtw16-ink: oklch(0.93 0.02 82);
--color-gtw16-mut: #a49a86;
--color-gtw16-mut: oklch(0.7 0.02 82);
--color-gtw16-line: #322c24;
--color-gtw16-line: oklch(0.31 0.015 78);
--color-gtw16-acc: #d9a04a;
--color-gtw16-acc: oklch(0.75 0.12 74);
--gtw16-up: rgba(0, 0, 0, 0.6);
--gtw16-down: rgba(255, 255, 255, 0.14);
--gtw16-fibre: 0.09;
}
.gtw-16:has(.gtw-16-theme:checked) {
--color-gtw16-stock: #efe9dc;
--color-gtw16-stock: oklch(0.92 0.02 84);
--color-gtw16-stock2: #f8f4ea;
--color-gtw16-stock2: oklch(0.96 0.015 85);
--color-gtw16-ink: #221f1a;
--color-gtw16-ink: oklch(0.24 0.01 70);
--color-gtw16-mut: #6b6355;
--color-gtw16-mut: oklch(0.47 0.015 80);
--color-gtw16-line: #d8cfbc;
--color-gtw16-line: oklch(0.86 0.02 84);
--color-gtw16-acc: #8c2f39;
--color-gtw16-acc: oklch(0.42 0.13 20);
--gtw16-up: rgba(34, 31, 26, 0.24);
--gtw16-down: rgba(255, 255, 255, 0.92);
--gtw16-fibre: 0.06;
}
}
@media (prefers-color-scheme: light) {
.gtw-16:has(.gtw-16-theme:checked) {
--color-gtw16-stock: #171512;
--color-gtw16-stock: oklch(0.19 0.01 70);
--color-gtw16-stock2: #201d19;
--color-gtw16-stock2: oklch(0.24 0.012 70);
--color-gtw16-ink: #f0e9da;
--color-gtw16-ink: oklch(0.93 0.02 82);
--color-gtw16-mut: #a49a86;
--color-gtw16-mut: oklch(0.7 0.02 82);
--color-gtw16-line: #322c24;
--color-gtw16-line: oklch(0.31 0.015 78);
--color-gtw16-acc: #d9a04a;
--color-gtw16-acc: oklch(0.75 0.12 74);
--gtw16-up: rgba(0, 0, 0, 0.6);
--gtw16-down: rgba(255, 255, 255, 0.14);
--gtw16-fibre: 0.09;
}
}<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=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&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=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The deboss is one long
[text-shadow:…]utility repeated on the five pressed elements. Repetition is the correct trade here: every one of those elements is pressed because it says so in its own class list, and the values come from shared variables so they cannot drift. - The fibre layer stays a
::beforeon the root, because it needsposition: fixedatz-index: 0behind every child — expressible as utilities only by adding a wrapper element the design does not need. hover:-rotate-[0.4deg]on the contact card is a one-utility version of what used to be a compoundtransform; v4's separaterotate/translateproperties compose without either overwriting the other.- UI change from the CSS original: shadow colours moved to
oklch()alpha, and the theme toggle now forwards focus from its input. The light-mode accent stays atoklch(0.42 0.13 20), which clears 4.5:1 on the stock for the small uppercase role line.
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 Paper Stock and Letterpress Press Effect
Source: https://codefronts.com/design-styles/tailwind-css-grain-texture/tailwind-paper-stock-letterpress-press-effect/
Letterpress in three parts: anisotropic fibre noise for the stock, a two-value text-shadow for the impression, and a deckle edge chewed by feDisplacementMap. One light source, declared once, shared by the type, the card edges and the paper itself — which is the only reason the effect is convincing.
## HTML
```html
<section class="gtw-16 relative block w-full min-h-svh bg-gtw16-stock text-gtw16-ink" aria-labelledby="gtw-16-title">
<svg class="absolute w-0 h-0 overflow-hidden" width="0" height="0" aria-hidden="true" focusable="false">
<filter id="gtw-16-fibre" x="0" y="0" width="100%" height="100%" color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.04 0.7" numOctaves="4" seed="14" stitchTiles="stitch"/>
<feColorMatrix type="saturate" values="0"/>
</filter>
<filter id="gtw-16-deckle" x="-15%" y="-15%" width="130%" height="130%" color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.02 0.4" numOctaves="3" seed="7" result="gtw16n"/>
<feDisplacementMap in="SourceGraphic" in2="gtw16n" scale="9" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</svg>
<div class="absolute top-[clamp(1rem,3vw,1.75rem)] right-[clamp(0.85rem,3vw,1.75rem)] z-40">
<input type="checkbox" id="gtw-16-theme" class="gtw-16-theme peer sr-only">
<label for="gtw-16-theme" class="grid place-items-center w-11 h-11 rounded-full border border-gtw16-line bg-gtw16-stock2 text-gtw16-ink cursor-pointer shadow-[inset_0_1px_0_var(--gtw16-down)] peer-focus-visible:outline-2 peer-focus-visible:outline-gtw16-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>
<main class="relative z-[1] w-[min(72rem,calc(100%-clamp(1.5rem,5vw,4rem)))] mx-auto pt-[clamp(3rem,8vw,5rem)] pb-[clamp(2.5rem,6vw,4rem)] grid gap-[clamp(2rem,5vw,3rem)]">
<header class="max-w-[44rem] grid gap-2">
<p class="m-0 font-gtw16-mono text-[0.72rem] leading-none tracking-[0.18em] uppercase text-gtw16-mut">Stock No. 4 · 600gsm cotton rag</p>
<h1 id="gtw-16-title" class="m-0 font-gtw16-display text-[clamp(2.4rem,7vw,4.4rem)] font-normal leading-[0.98] tracking-[-0.02em] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down),0_0_1px_color-mix(in_oklab,var(--color-gtw16-ink)_18%,transparent)]">Pressed, not printed</h1>
<p class="m-0 max-w-[38rem] text-gtw16-mut text-[clamp(0.95rem,1.9vw,1.06rem)] text-pretty">Directional fibre for the stock, a two-value <code class="font-gtw16-mono text-[0.9em]">text-shadow</code> for the press, and one light source shared by every layer.</p>
</header>
<article class="relative p-[clamp(0.5rem,2vw,1rem)]">
<div class="relative grid justify-items-center gap-[0.7rem] text-center px-[clamp(1.4rem,5vw,3.5rem)] py-[clamp(2.2rem,6vw,4rem)] bg-gtw16-stock2 shadow-[inset_0_1px_0_var(--gtw16-down),0_22px_50px_-30px_oklch(0.3_0.03_70/0.45)] [filter:url(#gtw-16-deckle)]">
<p aria-hidden="true" class="m-0 text-[1.1rem] text-gtw16-acc">✦</p>
<p class="m-0 font-gtw16-display text-[clamp(1.6rem,5vw,2.8rem)] font-normal leading-[1.1] tracking-[0.01em] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down)]">Aleksandra & Tomás</p>
<h2 class="m-0 max-w-[26rem] text-[clamp(0.95rem,2.2vw,1.15rem)] font-normal leading-[1.5] tracking-[0.06em] lowercase text-gtw16-mut">request the pleasure of your company</h2>
<p class="mt-[0.3rem] mb-0 font-gtw16-display text-[clamp(1.05rem,2.6vw,1.35rem)] font-normal leading-[1.5] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down)]">Saturday, the fourteenth of June<br>Two thousand and twenty-seven</p>
<div aria-hidden="true" class="w-12 h-px bg-gtw16-acc"></div>
<p class="m-0 font-gtw16-mono text-[0.78rem] leading-[1.6] tracking-[0.14em] uppercase text-gtw16-mut">Bracken Hall · Northumberland · 4 o'clock</p>
</div>
</article>
<div class="grid gap-[clamp(1.2rem,3vw,2rem)] grid-cols-[repeat(auto-fit,minmax(min(100%,18rem),1fr))] items-start">
<article class="grid content-center gap-[0.4rem] aspect-[85/55] p-[clamp(1.1rem,3vw,1.6rem)] rounded-sm bg-gtw16-stock2 shadow-[inset_0_1px_0_var(--gtw16-down),0_12px_30px_-20px_oklch(0.3_0.03_70/0.4)] transition-[translate,rotate,box-shadow] duration-[380ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-[3px] hover:-rotate-[0.4deg] motion-reduce:transition-none motion-reduce:hover:translate-y-0 motion-reduce:hover:rotate-0">
<p class="m-0 font-gtw16-display text-[clamp(1.3rem,3.4vw,1.8rem)] font-normal leading-[1.1] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down)]">Rowan Ellis</p>
<p class="m-0 font-gtw16-mono text-[0.72rem] leading-[1.4] tracking-[0.14em] uppercase text-gtw16-acc">Compositor & Press Operator</p>
<dl class="grid gap-[0.1rem] mt-2 mb-0 pt-[0.6rem] border-t border-gtw16-line">
<div class="flex gap-2"><dt class="m-0 min-w-[3.4rem] font-gtw16-mono text-[0.7rem] leading-[1.6] tracking-[0.1em] uppercase text-gtw16-mut">Studio</dt><dd class="m-0 text-[0.85rem]">Unit 7, Sackville Yard</dd></div>
<div class="flex gap-2"><dt class="m-0 min-w-[3.4rem] font-gtw16-mono text-[0.7rem] leading-[1.6] tracking-[0.1em] uppercase text-gtw16-mut">Post</dt><dd class="m-0 text-[0.85rem]">rowan@bracken.press</dd></div>
</dl>
</article>
<section class="grid gap-[0.7rem] p-[clamp(1.1rem,3vw,1.7rem)] rounded-sm border border-gtw16-line bg-[color-mix(in_oklab,var(--color-gtw16-stock2)_60%,transparent)]" aria-labelledby="gtw-16-h-spec">
<h2 id="gtw-16-h-spec" class="m-0 font-gtw16-display text-[clamp(1.2rem,3vw,1.6rem)] font-normal leading-[1.15] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down)]">Press specification</h2>
<dl class="grid gap-0 m-0">
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Stock</dt><dd class="m-0 text-[0.88rem]">600gsm cotton rag, deckle edge</dd></div>
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Fibre</dt><dd class="m-0 text-[0.88rem]">anisotropic turbulence · 0.04 0.7</dd></div>
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Impression</dt><dd class="m-0 text-[0.88rem]">text-shadow · 1px above, 1px below</dd></div>
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Ink bleed</dt><dd class="m-0 text-[0.88rem]">0 0 1px at 18% ink</dd></div>
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Light</dt><dd class="m-0 text-[0.88rem]">above, consistent across all layers</dd></div>
</dl>
<p class="m-0 text-gtw16-mut text-[0.83rem] text-pretty">Deboss below 13px hurts legibility. Cap the impression at your body size and drop it entirely for captions.</p>
</section>
</div>
</main>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-gtw16-stock: #efe9dc;
--color-gtw16-stock: oklch(0.92 0.02 84);
--color-gtw16-stock2: #f8f4ea;
--color-gtw16-stock2: oklch(0.96 0.015 85);
--color-gtw16-ink: #221f1a;
--color-gtw16-ink: oklch(0.24 0.01 70);
--color-gtw16-mut: #6b6355;
--color-gtw16-mut: oklch(0.47 0.015 80);
--color-gtw16-line: #d8cfbc;
--color-gtw16-line: oklch(0.86 0.02 84);
--color-gtw16-acc: #8c2f39;
--color-gtw16-acc: oklch(0.42 0.13 20);
--font-gtw16-display: 'Instrument Serif', 'Cormorant Garamond', 'Playfair Display', Georgia, serif;
--font-gtw16-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
.gtw-16 {
width: 100%;
min-height: 100svh;
--gtw16-press: 1px;
--gtw16-fibre: 0.06;
--gtw16-up: rgba(34, 31, 26, 0.24);
--gtw16-down: rgba(255, 255, 255, 0.92);
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.6;
}
/* The paper itself: a fixed, viewport-covering fibre layer. It is a ::before on the root
rather than a utility because it must sit behind every child at z-index 0 and never scroll. */
.gtw-16::before {
content: "";
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
filter: url(#gtw-16-fibre);
opacity: var(--gtw16-fibre);
mix-blend-mode: multiply;
}
.gtw-16:has(.gtw-16-theme:checked) {
--color-gtw16-stock: #171512;
--color-gtw16-stock: oklch(0.19 0.01 70);
--color-gtw16-stock2: #201d19;
--color-gtw16-stock2: oklch(0.24 0.012 70);
--color-gtw16-ink: #f0e9da;
--color-gtw16-ink: oklch(0.93 0.02 82);
--color-gtw16-mut: #a49a86;
--color-gtw16-mut: oklch(0.7 0.02 82);
--color-gtw16-line: #322c24;
--color-gtw16-line: oklch(0.31 0.015 78);
--color-gtw16-acc: #d9a04a;
--color-gtw16-acc: oklch(0.75 0.12 74);
--gtw16-up: rgba(0, 0, 0, 0.6);
--gtw16-down: rgba(255, 255, 255, 0.14);
--gtw16-fibre: 0.09;
}
@media (prefers-color-scheme: dark) {
.gtw-16 {
--color-gtw16-stock: #171512;
--color-gtw16-stock: oklch(0.19 0.01 70);
--color-gtw16-stock2: #201d19;
--color-gtw16-stock2: oklch(0.24 0.012 70);
--color-gtw16-ink: #f0e9da;
--color-gtw16-ink: oklch(0.93 0.02 82);
--color-gtw16-mut: #a49a86;
--color-gtw16-mut: oklch(0.7 0.02 82);
--color-gtw16-line: #322c24;
--color-gtw16-line: oklch(0.31 0.015 78);
--color-gtw16-acc: #d9a04a;
--color-gtw16-acc: oklch(0.75 0.12 74);
--gtw16-up: rgba(0, 0, 0, 0.6);
--gtw16-down: rgba(255, 255, 255, 0.14);
--gtw16-fibre: 0.09;
}
.gtw-16:has(.gtw-16-theme:checked) {
--color-gtw16-stock: #efe9dc;
--color-gtw16-stock: oklch(0.92 0.02 84);
--color-gtw16-stock2: #f8f4ea;
--color-gtw16-stock2: oklch(0.96 0.015 85);
--color-gtw16-ink: #221f1a;
--color-gtw16-ink: oklch(0.24 0.01 70);
--color-gtw16-mut: #6b6355;
--color-gtw16-mut: oklch(0.47 0.015 80);
--color-gtw16-line: #d8cfbc;
--color-gtw16-line: oklch(0.86 0.02 84);
--color-gtw16-acc: #8c2f39;
--color-gtw16-acc: oklch(0.42 0.13 20);
--gtw16-up: rgba(34, 31, 26, 0.24);
--gtw16-down: rgba(255, 255, 255, 0.92);
--gtw16-fibre: 0.06;
}
}
@media (prefers-color-scheme: light) {
.gtw-16:has(.gtw-16-theme:checked) {
--color-gtw16-stock: #171512;
--color-gtw16-stock: oklch(0.19 0.01 70);
--color-gtw16-stock2: #201d19;
--color-gtw16-stock2: oklch(0.24 0.012 70);
--color-gtw16-ink: #f0e9da;
--color-gtw16-ink: oklch(0.93 0.02 82);
--color-gtw16-mut: #a49a86;
--color-gtw16-mut: oklch(0.7 0.02 82);
--color-gtw16-line: #322c24;
--color-gtw16-line: oklch(0.31 0.015 78);
--color-gtw16-acc: #d9a04a;
--color-gtw16-acc: oklch(0.75 0.12 74);
--gtw16-up: rgba(0, 0, 0, 0.6);
--gtw16-down: rgba(255, 255, 255, 0.14);
--gtw16-fibre: 0.09;
}
}
```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 Paper Stock and Letterpress Press Effect
Source: https://codefronts.com/design-styles/tailwind-css-grain-texture/tailwind-paper-stock-letterpress-press-effect/
Letterpress in three parts: anisotropic fibre noise for the stock, a two-value text-shadow for the impression, and a deckle edge chewed by feDisplacementMap. One light source, declared once, shared by the type, the card edges and the paper itself — which is the only reason the effect is convincing.
## HTML
```html
<section class="gtw-16 relative block w-full min-h-svh bg-gtw16-stock text-gtw16-ink" aria-labelledby="gtw-16-title">
<svg class="absolute w-0 h-0 overflow-hidden" width="0" height="0" aria-hidden="true" focusable="false">
<filter id="gtw-16-fibre" x="0" y="0" width="100%" height="100%" color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.04 0.7" numOctaves="4" seed="14" stitchTiles="stitch"/>
<feColorMatrix type="saturate" values="0"/>
</filter>
<filter id="gtw-16-deckle" x="-15%" y="-15%" width="130%" height="130%" color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.02 0.4" numOctaves="3" seed="7" result="gtw16n"/>
<feDisplacementMap in="SourceGraphic" in2="gtw16n" scale="9" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</svg>
<div class="absolute top-[clamp(1rem,3vw,1.75rem)] right-[clamp(0.85rem,3vw,1.75rem)] z-40">
<input type="checkbox" id="gtw-16-theme" class="gtw-16-theme peer sr-only">
<label for="gtw-16-theme" class="grid place-items-center w-11 h-11 rounded-full border border-gtw16-line bg-gtw16-stock2 text-gtw16-ink cursor-pointer shadow-[inset_0_1px_0_var(--gtw16-down)] peer-focus-visible:outline-2 peer-focus-visible:outline-gtw16-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>
<main class="relative z-[1] w-[min(72rem,calc(100%-clamp(1.5rem,5vw,4rem)))] mx-auto pt-[clamp(3rem,8vw,5rem)] pb-[clamp(2.5rem,6vw,4rem)] grid gap-[clamp(2rem,5vw,3rem)]">
<header class="max-w-[44rem] grid gap-2">
<p class="m-0 font-gtw16-mono text-[0.72rem] leading-none tracking-[0.18em] uppercase text-gtw16-mut">Stock No. 4 · 600gsm cotton rag</p>
<h1 id="gtw-16-title" class="m-0 font-gtw16-display text-[clamp(2.4rem,7vw,4.4rem)] font-normal leading-[0.98] tracking-[-0.02em] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down),0_0_1px_color-mix(in_oklab,var(--color-gtw16-ink)_18%,transparent)]">Pressed, not printed</h1>
<p class="m-0 max-w-[38rem] text-gtw16-mut text-[clamp(0.95rem,1.9vw,1.06rem)] text-pretty">Directional fibre for the stock, a two-value <code class="font-gtw16-mono text-[0.9em]">text-shadow</code> for the press, and one light source shared by every layer.</p>
</header>
<article class="relative p-[clamp(0.5rem,2vw,1rem)]">
<div class="relative grid justify-items-center gap-[0.7rem] text-center px-[clamp(1.4rem,5vw,3.5rem)] py-[clamp(2.2rem,6vw,4rem)] bg-gtw16-stock2 shadow-[inset_0_1px_0_var(--gtw16-down),0_22px_50px_-30px_oklch(0.3_0.03_70/0.45)] [filter:url(#gtw-16-deckle)]">
<p aria-hidden="true" class="m-0 text-[1.1rem] text-gtw16-acc">✦</p>
<p class="m-0 font-gtw16-display text-[clamp(1.6rem,5vw,2.8rem)] font-normal leading-[1.1] tracking-[0.01em] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down)]">Aleksandra & Tomás</p>
<h2 class="m-0 max-w-[26rem] text-[clamp(0.95rem,2.2vw,1.15rem)] font-normal leading-[1.5] tracking-[0.06em] lowercase text-gtw16-mut">request the pleasure of your company</h2>
<p class="mt-[0.3rem] mb-0 font-gtw16-display text-[clamp(1.05rem,2.6vw,1.35rem)] font-normal leading-[1.5] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down)]">Saturday, the fourteenth of June<br>Two thousand and twenty-seven</p>
<div aria-hidden="true" class="w-12 h-px bg-gtw16-acc"></div>
<p class="m-0 font-gtw16-mono text-[0.78rem] leading-[1.6] tracking-[0.14em] uppercase text-gtw16-mut">Bracken Hall · Northumberland · 4 o'clock</p>
</div>
</article>
<div class="grid gap-[clamp(1.2rem,3vw,2rem)] grid-cols-[repeat(auto-fit,minmax(min(100%,18rem),1fr))] items-start">
<article class="grid content-center gap-[0.4rem] aspect-[85/55] p-[clamp(1.1rem,3vw,1.6rem)] rounded-sm bg-gtw16-stock2 shadow-[inset_0_1px_0_var(--gtw16-down),0_12px_30px_-20px_oklch(0.3_0.03_70/0.4)] transition-[translate,rotate,box-shadow] duration-[380ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-[3px] hover:-rotate-[0.4deg] motion-reduce:transition-none motion-reduce:hover:translate-y-0 motion-reduce:hover:rotate-0">
<p class="m-0 font-gtw16-display text-[clamp(1.3rem,3.4vw,1.8rem)] font-normal leading-[1.1] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down)]">Rowan Ellis</p>
<p class="m-0 font-gtw16-mono text-[0.72rem] leading-[1.4] tracking-[0.14em] uppercase text-gtw16-acc">Compositor & Press Operator</p>
<dl class="grid gap-[0.1rem] mt-2 mb-0 pt-[0.6rem] border-t border-gtw16-line">
<div class="flex gap-2"><dt class="m-0 min-w-[3.4rem] font-gtw16-mono text-[0.7rem] leading-[1.6] tracking-[0.1em] uppercase text-gtw16-mut">Studio</dt><dd class="m-0 text-[0.85rem]">Unit 7, Sackville Yard</dd></div>
<div class="flex gap-2"><dt class="m-0 min-w-[3.4rem] font-gtw16-mono text-[0.7rem] leading-[1.6] tracking-[0.1em] uppercase text-gtw16-mut">Post</dt><dd class="m-0 text-[0.85rem]">rowan@bracken.press</dd></div>
</dl>
</article>
<section class="grid gap-[0.7rem] p-[clamp(1.1rem,3vw,1.7rem)] rounded-sm border border-gtw16-line bg-[color-mix(in_oklab,var(--color-gtw16-stock2)_60%,transparent)]" aria-labelledby="gtw-16-h-spec">
<h2 id="gtw-16-h-spec" class="m-0 font-gtw16-display text-[clamp(1.2rem,3vw,1.6rem)] font-normal leading-[1.15] [text-shadow:0_calc(var(--gtw16-press)*-1)_0_var(--gtw16-up),0_var(--gtw16-press)_0_var(--gtw16-down)]">Press specification</h2>
<dl class="grid gap-0 m-0">
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Stock</dt><dd class="m-0 text-[0.88rem]">600gsm cotton rag, deckle edge</dd></div>
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Fibre</dt><dd class="m-0 text-[0.88rem]">anisotropic turbulence · 0.04 0.7</dd></div>
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Impression</dt><dd class="m-0 text-[0.88rem]">text-shadow · 1px above, 1px below</dd></div>
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Ink bleed</dt><dd class="m-0 text-[0.88rem]">0 0 1px at 18% ink</dd></div>
<div class="grid grid-cols-[minmax(6rem,8rem)_minmax(0,1fr)] gap-[0.6rem] py-2 border-t border-gtw16-line"><dt class="m-0 font-gtw16-mono text-[0.7rem] leading-[1.7] tracking-[0.1em] uppercase text-gtw16-mut">Light</dt><dd class="m-0 text-[0.88rem]">above, consistent across all layers</dd></div>
</dl>
<p class="m-0 text-gtw16-mut text-[0.83rem] text-pretty">Deboss below 13px hurts legibility. Cap the impression at your body size and drop it entirely for captions.</p>
</section>
</div>
</main>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-gtw16-stock: #efe9dc;
--color-gtw16-stock: oklch(0.92 0.02 84);
--color-gtw16-stock2: #f8f4ea;
--color-gtw16-stock2: oklch(0.96 0.015 85);
--color-gtw16-ink: #221f1a;
--color-gtw16-ink: oklch(0.24 0.01 70);
--color-gtw16-mut: #6b6355;
--color-gtw16-mut: oklch(0.47 0.015 80);
--color-gtw16-line: #d8cfbc;
--color-gtw16-line: oklch(0.86 0.02 84);
--color-gtw16-acc: #8c2f39;
--color-gtw16-acc: oklch(0.42 0.13 20);
--font-gtw16-display: 'Instrument Serif', 'Cormorant Garamond', 'Playfair Display', Georgia, serif;
--font-gtw16-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
.gtw-16 {
width: 100%;
min-height: 100svh;
--gtw16-press: 1px;
--gtw16-fibre: 0.06;
--gtw16-up: rgba(34, 31, 26, 0.24);
--gtw16-down: rgba(255, 255, 255, 0.92);
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.6;
}
/* The paper itself: a fixed, viewport-covering fibre layer. It is a ::before on the root
rather than a utility because it must sit behind every child at z-index 0 and never scroll. */
.gtw-16::before {
content: "";
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
filter: url(#gtw-16-fibre);
opacity: var(--gtw16-fibre);
mix-blend-mode: multiply;
}
.gtw-16:has(.gtw-16-theme:checked) {
--color-gtw16-stock: #171512;
--color-gtw16-stock: oklch(0.19 0.01 70);
--color-gtw16-stock2: #201d19;
--color-gtw16-stock2: oklch(0.24 0.012 70);
--color-gtw16-ink: #f0e9da;
--color-gtw16-ink: oklch(0.93 0.02 82);
--color-gtw16-mut: #a49a86;
--color-gtw16-mut: oklch(0.7 0.02 82);
--color-gtw16-line: #322c24;
--color-gtw16-line: oklch(0.31 0.015 78);
--color-gtw16-acc: #d9a04a;
--color-gtw16-acc: oklch(0.75 0.12 74);
--gtw16-up: rgba(0, 0, 0, 0.6);
--gtw16-down: rgba(255, 255, 255, 0.14);
--gtw16-fibre: 0.09;
}
@media (prefers-color-scheme: dark) {
.gtw-16 {
--color-gtw16-stock: #171512;
--color-gtw16-stock: oklch(0.19 0.01 70);
--color-gtw16-stock2: #201d19;
--color-gtw16-stock2: oklch(0.24 0.012 70);
--color-gtw16-ink: #f0e9da;
--color-gtw16-ink: oklch(0.93 0.02 82);
--color-gtw16-mut: #a49a86;
--color-gtw16-mut: oklch(0.7 0.02 82);
--color-gtw16-line: #322c24;
--color-gtw16-line: oklch(0.31 0.015 78);
--color-gtw16-acc: #d9a04a;
--color-gtw16-acc: oklch(0.75 0.12 74);
--gtw16-up: rgba(0, 0, 0, 0.6);
--gtw16-down: rgba(255, 255, 255, 0.14);
--gtw16-fibre: 0.09;
}
.gtw-16:has(.gtw-16-theme:checked) {
--color-gtw16-stock: #efe9dc;
--color-gtw16-stock: oklch(0.92 0.02 84);
--color-gtw16-stock2: #f8f4ea;
--color-gtw16-stock2: oklch(0.96 0.015 85);
--color-gtw16-ink: #221f1a;
--color-gtw16-ink: oklch(0.24 0.01 70);
--color-gtw16-mut: #6b6355;
--color-gtw16-mut: oklch(0.47 0.015 80);
--color-gtw16-line: #d8cfbc;
--color-gtw16-line: oklch(0.86 0.02 84);
--color-gtw16-acc: #8c2f39;
--color-gtw16-acc: oklch(0.42 0.13 20);
--gtw16-up: rgba(34, 31, 26, 0.24);
--gtw16-down: rgba(255, 255, 255, 0.92);
--gtw16-fibre: 0.06;
}
}
@media (prefers-color-scheme: light) {
.gtw-16:has(.gtw-16-theme:checked) {
--color-gtw16-stock: #171512;
--color-gtw16-stock: oklch(0.19 0.01 70);
--color-gtw16-stock2: #201d19;
--color-gtw16-stock2: oklch(0.24 0.012 70);
--color-gtw16-ink: #f0e9da;
--color-gtw16-ink: oklch(0.93 0.02 82);
--color-gtw16-mut: #a49a86;
--color-gtw16-mut: oklch(0.7 0.02 82);
--color-gtw16-line: #322c24;
--color-gtw16-line: oklch(0.31 0.015 78);
--color-gtw16-acc: #d9a04a;
--color-gtw16-acc: oklch(0.75 0.12 74);
--gtw16-up: rgba(0, 0, 0, 0.6);
--gtw16-down: rgba(255, 255, 255, 0.14);
--gtw16-fibre: 0.09;
}
}
```How this works
Deboss is two shadows and a lie about light. A dark shadow 1px above the glyph and a light one 1px below tells the eye the surface is pushed in, because that is what a top-lit indentation does. Swap them and the same type reads embossed. The third shadow — a 1px blur at 18% ink — is the bleed where ink wicks into paper fibre, and it is what stops the effect looking like a CSS trick.
The light source has to be shared. --gtw16-up and --gtw16-down drive the type impression, the card's inset highlight and the toggle's rim. If any one of them implies light from a different direction, the whole page stops reading as physical — and that consistency is far more important than the individual values.
Fibre is anisotropic noise. baseFrequency="0.04 0.7" stretches the lattice into streaks, which is how cotton rag actually looks under raking light. Isotropic grain at the same opacity reads as dirt on a screen rather than as material.
The deckle edge is the same displacement trick as demo 13, applied to a card instead of a band: low-frequency noise pushing the panel's own edges around by up to 9 units, with a 30% oversized filter region so the displacement is not clipped.
Make it yours
- Impression depth:
--gtw16-press(1px). 2px reads as a deep press on display sizes; never above 1px under about 20px type. - Light:
--gtw16-up/--gtw16-down. On dark stock the up-shadow needs to be much stronger (0.6 vs 0.24) and the down-highlight much weaker. - Fibre:
--gtw16-fibre(0.06 light / 0.09 dark) and the filter's two-valuebaseFrequency. - Deckle:
scale="9"on the displacement map. Raise for handmade stock, remove the filter entirely for guillotined edges. - Swap the invitation copy for your own — the press effect is independent of the words, and the card's aspect ratio is
85/55(business-card standard) on the contact card.
Gotchas — read before shipping
text-shadowdeboss below about 13px makes type harder to read, not more tactile. Cap it at your body size and drop it for captions — the demo says so in its own spec panel.- Deboss shadows that disagree about light direction destroy the illusion instantly. Drive them all from one variable pair.
- A
fixedfibre layer on the root is correct for a full-page stock but wrong inside a scrolling panel — there it must beabsolutewithin the panel, or the texture slides against the content. filter: url()on a card creates a containing block: absolutely positioned children inside it resolve against the card, not the page. Usually what you want, occasionally a surprise.- Deckle displacement on an element with a
box-shadowdisplaces the shadow too, which can look like a rendering error at high scale values. Keep scale under about 12 on shadowed cards.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
text-shadow, SVG filters and feDisplacementMap are Baseline; color-mix() and Tailwind v4's generated sheet set the quoted floor. Safari renders feTurbulence slightly darker, so check the fibre opacity there.