Textured Section Backgrounds (Pure CSS)
⚠️ Cookbook example — not live code. (KEEP THIS CALLOUT.) Every code block below is an example snippet, not part of the llamapress.ai codebase, and not running on this server. This is a reference recipe for a Leo instance (an AI coding agent) to implement in its own app — read it to understand the pattern, then recreate it there.
Flat bg-white / bg-slate-50 sections read as unfinished. A faint pattern, a glow, and
2–5% noise make the same layout feel designed. Every recipe here is pure CSS or an
inline SVG data URI — no image files, no build step, no new dependencies. They work in
any Rails view with Tailwind (Play CDN or compiled).
When to use: landing/marketing pages, hero sections, CTA breaks, empty states, dashboards that need a branded feel. When not to: dense data screens (tables, forms) — texture behind small text hurts readability. And never as a substitute for good spacing/hierarchy; texture is the last 10%, not the first 90%.
The 80/20 in one breath
- Every textured section is the same skeleton: a
relative overflow-hiddensection, one or moreabsolute inset-0pattern layer divs, then arelativecontent wrapper on top. - Use CSS gradients for anything mathematical (dots, grids, stripes, rings) and a
tiny inline SVG
feTurbulencedata URI for noise/grain. Real image files only for organic textures (paper fiber, watercolor). - Put the pattern CSS in inline
style=""attributes, not Tailwind arbitrary-value classes — see Gotchas. - Add a
mask-imagefade (radial or linear) so the pattern dissolves instead of ending abruptly — the mask does most of the aesthetic work. - Keep opacity LOW. If a viewer consciously notices the texture, it's too strong.
- The strongest default modern look: base color + radial glow + fading dots/grid + 3–5% noise (recipe 5 below).
Layer 1 — The skeleton (used by every recipe)
<%# app/views/pages/home.html.erb (or any view/partial) — the shared wrapper shape %>
<section class="relative overflow-hidden bg-white">
<!-- pattern layer(s): absolute, behind content, never interactive -->
<div class="pointer-events-none absolute inset-0" style="/* pattern CSS here */"></div>
<!-- content: relative so it stacks above the pattern -->
<div class="relative max-w-6xl mx-auto px-6 py-24">
...
</div>
</section>
The noise texture used by several recipes is this inline SVG data URI (define it once — an ERB local, a helper, or just paste the string):
<%# Tiny feTurbulence noise tile as a data URI — no asset file needed. %>
<% noise_svg = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E" %>
Layer 2 — Light-background patterns
Each block below is the pattern layer div only — drop it into the skeleton above.
1. Sparse dots — friendly, neutral sections:
<!-- pattern layer: sparse dots -->
<div class="pointer-events-none absolute inset-0 opacity-40"
style="background-image: radial-gradient(circle, #94a3b8 1px, transparent 1px); background-size: 22px 22px;"></div>
2. Subtle grid — technical/product sections (two crossed 1px gradients, ~6% black):
<!-- pattern layer: subtle grid -->
<div class="pointer-events-none absolute inset-0"
style="background-image: linear-gradient(to right, rgba(15,23,42,0.06) 1px, transparent 1px), linear-gradient(to bottom, rgba(15,23,42,0.06) 1px, transparent 1px); background-size: 32px 32px;"></div>
3. Fading grid (radial mask) — the modern upgrade of #2; the fade IS the design:
<!-- pattern layer: grid that dissolves toward the edges -->
<div class="pointer-events-none absolute inset-0"
style="background-image: linear-gradient(to right, rgba(15,23,42,0.09) 1px, transparent 1px), linear-gradient(to bottom, rgba(15,23,42,0.09) 1px, transparent 1px); background-size: 32px 32px; -webkit-mask-image: radial-gradient(ellipse at center, black, transparent 72%); mask-image: radial-gradient(ellipse at center, black, transparent 72%);"></div>
4. Fine diagonal stripes — callouts/comparison blocks; tint to the surface color:
<!-- pattern layer: fine 135° stripes (amber-tinted example) -->
<div class="pointer-events-none absolute inset-0 opacity-60"
style="background-image: repeating-linear-gradient(135deg, rgba(120,53,15,0.06) 0, rgba(120,53,15,0.06) 1px, transparent 1px, transparent 10px);"></div>
11. Plus / cross grid — the Vercel/Linear look; tiny "+" marks from an inline SVG:
<!-- pattern layer: "+" tile, masked to fade at edges. The data URI draws one plus. -->
<div class="pointer-events-none absolute inset-0 opacity-50"
style="background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28'%3E%3Cpath d='M14 10v8M10 14h8' stroke='%2394a3b8' stroke-width='1.2' stroke-linecap='round'/%3E%3C/svg%3E"); background-size: 28px 28px; -webkit-mask-image: radial-gradient(ellipse at center, black, transparent 75%); mask-image: radial-gradient(ellipse at center, black, transparent 75%);"></div>
12. Graph paper — major + minor gridlines in ONE element (4 layers, 2 sizes):
<!-- pattern layer: 16px minor grid + 80px major grid -->
<div class="pointer-events-none absolute inset-0"
style="background-image: linear-gradient(to right, rgba(15,23,42,0.09) 1px, transparent 1px), linear-gradient(to bottom, rgba(15,23,42,0.09) 1px, transparent 1px), linear-gradient(to right, rgba(15,23,42,0.04) 1px, transparent 1px), linear-gradient(to bottom, rgba(15,23,42,0.04) 1px, transparent 1px); background-size: 80px 80px, 80px 80px, 16px 16px, 16px 16px;"></div>
13. Halftone fade — dots that shrink as they descend (3 sizes, staggered masks):
<!-- pattern layers: 2.4px → 1.4px → 0.8px dots, each masked to its own vertical band -->
<div class="pointer-events-none absolute inset-0 opacity-25"
style="background-image: radial-gradient(circle, #475569 2.4px, transparent 2.4px); background-size: 16px 16px; -webkit-mask-image: linear-gradient(to bottom, black, transparent 45%); mask-image: linear-gradient(to bottom, black, transparent 45%);"></div>
<div class="pointer-events-none absolute inset-0 opacity-25"
style="background-image: radial-gradient(circle, #475569 1.4px, transparent 1.4px); background-size: 16px 16px; -webkit-mask-image: linear-gradient(to bottom, transparent 15%, black 45%, transparent 75%); mask-image: linear-gradient(to bottom, transparent 15%, black 45%, transparent 75%);"></div>
<div class="pointer-events-none absolute inset-0 opacity-25"
style="background-image: radial-gradient(circle, #475569 0.8px, transparent 0.8px); background-size: 16px 16px; -webkit-mask-image: linear-gradient(to bottom, transparent 55%, black 85%, transparent); mask-image: linear-gradient(to bottom, transparent 55%, black 85%, transparent);"></div>
7. Contour rings — faux-topographic, premium editorial; anchor off-center:
<!-- pattern layer: repeating concentric rings -->
<div class="pointer-events-none absolute inset-0 opacity-50"
style="background-image: repeating-radial-gradient(circle at 80% 20%, rgba(6,95,70,0.08) 0, rgba(6,95,70,0.08) 1px, transparent 1px, transparent 48px);"></div>
10. Checkerboard fading upward — CTA/footer transitions:
<!-- pattern layer: conic checkerboard, visible only near the bottom edge -->
<div class="pointer-events-none absolute inset-0 opacity-35"
style="background-image: conic-gradient(rgba(15,23,42,0.06) 90deg, transparent 90deg 180deg, rgba(15,23,42,0.06) 180deg 270deg, transparent 270deg); background-size: 48px 48px; -webkit-mask-image: linear-gradient(to top, black, transparent 85%); mask-image: linear-gradient(to top, black, transparent 85%);"></div>
Layer 3 — Dark heroes
5. Glow + fading dots + noise — the strongest default modern look. Three stacked
layers on bg-slate-950:
<!-- on <section class="relative overflow-hidden bg-slate-950 text-white"> -->
<!-- layer 1: radial glow at top -->
<div class="pointer-events-none absolute inset-0"
style="background-image: radial-gradient(circle at 50% 0%, rgba(99,102,241,0.25), transparent 45%);"></div>
<!-- layer 2: dots fading downward -->
<div class="pointer-events-none absolute inset-0 opacity-20"
style="background-image: radial-gradient(circle, rgba(255,255,255,0.3) 1px, transparent 1px); background-size: 24px 24px; -webkit-mask-image: linear-gradient(to bottom, black, transparent 80%); mask-image: linear-gradient(to bottom, black, transparent 80%);"></div>
<!-- layer 3: 5% noise grain (noise_svg from Layer 1 above) -->
<div class="pointer-events-none absolute inset-0 mix-blend-soft-light"
style="background-image: url("<%= noise_svg %>"); opacity: 0.05;"></div>
14. Spotlight beams — conic-gradient light shafts anchored above the viewport:
<!-- pattern layer: three beams from above, fading down; add the noise layer on top -->
<div class="pointer-events-none absolute inset-0"
style="background-image: conic-gradient(from 165deg at 30% -10%, transparent 0deg, rgba(99,102,241,0.18) 12deg, transparent 30deg), conic-gradient(from 195deg at 70% -10%, transparent 0deg, rgba(56,189,248,0.14) 14deg, transparent 32deg), conic-gradient(from 178deg at 50% -15%, transparent 0deg, rgba(255,255,255,0.06) 8deg, transparent 20deg); -webkit-mask-image: linear-gradient(to bottom, black 40%, transparent 95%); mask-image: linear-gradient(to bottom, black 40%, transparent 95%);"></div>
18. Perspective horizon grid — a grid tilted into a 3D floor. Strong flavor; one per site, if at all:
<!-- bottom two-thirds of a dark section: grid rotated away from the viewer -->
<div class="absolute inset-x-0 bottom-0 h-2/3" style="perspective: 400px;">
<div class="pointer-events-none absolute inset-0"
style="background-image: linear-gradient(to right, rgba(129,140,248,0.25) 1px, transparent 1px), linear-gradient(to bottom, rgba(129,140,248,0.25) 1px, transparent 1px); background-size: 48px 48px; transform: rotateX(62deg); transform-origin: top; -webkit-mask-image: linear-gradient(to bottom, transparent, black 35%); mask-image: linear-gradient(to bottom, transparent, black 35%);"></div>
</div>
<div class="pointer-events-none absolute inset-0"
style="background-image: radial-gradient(circle at 50% 100%, rgba(99,102,241,0.25), transparent 55%);"></div>
Layer 4 — Color washes & grain
6. Grain over a gradient — noise is the highest-leverage texture; ~8–12% on gradients:
<section class="relative overflow-hidden text-white"
style="background-image: linear-gradient(135deg, #1d4ed8 0%, #7c3aed 100%);">
<div class="pointer-events-none absolute inset-0 mix-blend-soft-light"
style="background-image: url("<%= noise_svg %>"); opacity: 0.12;"></div>
<div class="relative max-w-6xl mx-auto px-6 py-20">...</div>
</section>
15. Mesh / aurora gradient — the Stripe-style wash; four radial-gradients at different corners, finished with grain:
<section class="relative overflow-hidden"
style="background-color: #fafaf9; background-image: radial-gradient(at 15% 25%, rgba(125,211,252,0.5) 0px, transparent 50%), radial-gradient(at 85% 15%, rgba(216,180,254,0.45) 0px, transparent 50%), radial-gradient(at 70% 85%, rgba(253,186,116,0.35) 0px, transparent 50%), radial-gradient(at 25% 90%, rgba(167,243,208,0.4) 0px, transparent 50%);">
<div class="pointer-events-none absolute inset-0 mix-blend-soft-light"
style="background-image: url("<%= noise_svg %>"); opacity: 0.06;"></div>
<div class="relative max-w-6xl mx-auto px-6 py-24">...</div>
</section>
8. Soft blobs — consumer/friendly surfaces; blurred circles at ~30% opacity:
<!-- pattern layers: three heavily blurred color fields -->
<div class="pointer-events-none absolute -top-24 -left-24 w-96 h-96 rounded-full bg-pink-300 opacity-30" style="filter: blur(90px);"></div>
<div class="pointer-events-none absolute -bottom-32 right-0 w-96 h-96 rounded-full bg-sky-300 opacity-30" style="filter: blur(100px);"></div>
<div class="pointer-events-none absolute top-1/3 left-1/2 w-72 h-72 rounded-full bg-amber-200 opacity-30" style="filter: blur(80px);"></div>
16. Animated aurora + glass panel — blobs drifting on slow transform loops, with a frosted card giving text a clean landing zone on top:
<!-- in a <style> tag or your CSS: transform-only keyframes (GPU-cheap) -->
<style>
@keyframes aurora-a { 0%,100% { transform: translate(0,0) scale(1); } 33% { transform: translate(50px,-35px) scale(1.15); } 66% { transform: translate(-35px,25px) scale(0.95); } }
@keyframes aurora-b { 0%,100% { transform: translate(0,0) scale(1); } 40% { transform: translate(-45px,30px) scale(1.1); } 70% { transform: translate(30px,-20px) scale(0.9); } }
</style>
<section class="relative overflow-hidden bg-indigo-50">
<div class="pointer-events-none absolute -top-20 -left-20 w-96 h-96 rounded-full bg-violet-300 opacity-40"
style="filter: blur(80px); animation: aurora-a 18s ease-in-out infinite;"></div>
<div class="pointer-events-none absolute top-10 right-0 w-96 h-96 rounded-full bg-sky-300 opacity-40"
style="filter: blur(90px); animation: aurora-b 24s ease-in-out infinite;"></div>
<div class="relative max-w-6xl mx-auto px-6 py-24">
<!-- the glass panel: how content sits on ANY texture -->
<div class="max-w-xl rounded-3xl border border-white/60 bg-white/60 backdrop-blur-xl shadow-lg p-8">
...
</div>
</div>
</section>
Layer 5 — Editorial / print textures
9. Paper grain on light — same noise SVG at 5% on multiply over warm stone
(bg-stone-100); almost invisible on purpose:
<div class="pointer-events-none absolute inset-0 mix-blend-multiply"
style="background-image: url("<%= noise_svg %>"); opacity: 0.05;"></div>
17. Ruled editorial lines — notebook rules + a red margin line; pair with a serif headline. Good for about pages and founder letters:
<section class="relative overflow-hidden" style="background-color: #fdfcf8;">
<div class="pointer-events-none absolute inset-0"
style="background-image: repeating-linear-gradient(to bottom, transparent 0, transparent 31px, rgba(120,113,108,0.14) 31px, rgba(120,113,108,0.14) 32px);"></div>
<div class="pointer-events-none absolute inset-y-0 hidden md:block" style="left: 88px; width: 1px; background: rgba(244,63,94,0.25);"></div>
<div class="relative max-w-6xl mx-auto px-6 md:pl-32 py-20">...</div>
</section>
Gotchas (the hard-won stuff)
- Use inline
style=""for the pattern CSS, not Tailwind arbitrary-value classes. Arbitrary values like[background-image:radial-gradient(...)]only work with the Play CDN or a JIT build that saw the class. Inline styles work everywhere — including boxes with compiled Tailwind — and survive copy-paste between apps. Use Tailwind classes only for layout/positioning (absolute inset-0 opacity-40). - Always ship
mask-imagewith the-webkit-mask-imagetwin. Chrome/Edge/older Safari need the prefix; without it the fade silently disappears and the pattern ends in a hard edge. pointer-events-noneon every pattern layer. Anabsolute inset-0div without it sits on top of your content in hit-testing and eats every click. This is the #1 way a texture "breaks the page."- The skeleton ordering matters: section is
relative overflow-hidden, pattern layers areabsolute, content wrapper isrelative. Forgetrelativeon the content and it z-fights with (or hides under) the pattern. Forgetoverflow-hiddenand off-canvas blobs create horizontal scrollbars. - Quoting the data URI inside an inline style in ERB/HTML: the style attribute is
delimited with double quotes, so wrap the
url(...)value in"..."(or single-quote the attribute). The SVG itself must be URL-encoded (%3Cfor<,%23for#,%25for%). - Opacity is the whole game. Noise: 2–5% on dark, 3–8% on gradients, 1–3% on white. Line/dot patterns: keep line color under ~10% opacity black. If you notice the texture, halve the opacity.
- Don't texture every section. A typical page: hero gets the full 3-layer treatment, one mid-page break gets dots or a grid, the CTA gets grain + gradient. Everything else stays plain — that contrast is what makes the textured sections land.
- Dense patterns cause moiré (shimmer while scrolling). Keep tiles ≥ 16px, dots ≥ 1px, and test on a low-DPI screen if you go finer.
- Animate
transformonly (like the aurora keyframes). Animatingbackground-positionorfilterrepaints every frame and melts low-end machines. 18–30s loops read as "alive," anything faster reads as "loading spinner." - Text needs a landing zone. Never put small text straight on a busy pattern — use a
mask to clear the center, or a
backdrop-blurglass panel (recipe 16). mix-blend-soft-lightneeds a stacking context that includes the background. If the noise looks like a flat gray sheet, the blend is compositing against the wrong layer — make sure the noise div is a sibling inside the section that owns the background color.
Files this pattern touches
app/views/<any page>.html.erb # recipes drop into existing views
app/views/shared/_textured_section.html.erb # optional: extract the skeleton as a partial
No migrations, no routes, no JavaScript dependencies, no image assets.
How to adapt to your schema
- Pick 3–4 recipes max for a site (e.g. dark hero #5, plus grid #11, mesh #15, grain #6) — a restrained set reads as a design system; using all 18 reads as a demo page.
- Swap the colors: replace the indigo/sky RGBA values with your brand palette. Keep the alpha values — they're tuned; change only the RGB.
- For a reusable system, extract the skeleton into a shared partial that takes the
pattern layer as a block or a
pattern:key, so every page speaks the same visual language. - Light vs dark: each recipe states its intended surface. To flip one, invert the
pattern color (dark lines on light ↔
rgba(255,255,255,…)on dark) and re-tune opacity down. - Safe to drop: the noise layer (pure garnish), the animation (static blobs still look
good), and the
-webkit-prefixes only if you know your users are on current Safari.