Building a whole Tailwind app, not just this component?
You just browsed 20 Tailwind grain texture. Untitled UI Pro unlocks 5,000+ components and sections and 250+ page examples for React, styled with Tailwind CSS v4. One licence is a one-time $349 with lifetime updates; teams start at $999.
Every grain effect on the modern web comes out of one SVG primitive: <feTurbulence>. This is the parameter sheet for it — six live plates, each changing exactly one attribute, plus a zero-JS strength rail that re-tunes all six at once. The port's win is that the entire grain layer (content, inset, filter, blend mode, opacity) reads as one after: class list on the surface that owns it.
A specimen sheet for the only two attributes that decide whether grain reads as expensive film stock or as a broken JPEG: seven baseFrequency steps, a four-step octave ramp, the two-value anisotropic form, and the min-resolution: 2dppx correction almost nobody ships. The port's interesting problem is the blend mode — it has to differ per theme, so it stays a custom property read back through an arbitrary utility.
The shippable form of browser-generated grain: the whole <feTurbulence> document folded into a URL-encoded data URI, parked in one custom property, and reused across a hero, three cards, two buttons and a footer. 382 bytes, zero network requests, and in v4 every surface reads it as bg-[image:var(--gtw03-noise),…] with the blend stack stated right beside it.
Grain has two dials after generation: how strong it is and how it composites. This is the full matrix — eight mix-blend-mode values against one identical noise layer, plus a five-stop opacity scale with the production band marked. The port deletes every inline style attribute the CSS version needed: each mode is a real utility, and isolate sits on the surface that owns the blend.
Raw feTurbulence writes independent noise into red, green and blue, so untinted grain is colour confetti. feColorMatrix fixes it properly: flatten RGB to one hue, then move the texture into the alpha channel so the layer composites in normal blend mode over anything. Six named stocks, and the port's point is that the filter chain is the only part that cannot become a utility — so everything else does.
Everyone repeats that SVG filters are expensive. Nobody says how expensive. This demo measures it live in your own browser: median frame time for four surfaces — no grain, cached data URI, live 5-octave filter, six blended layers — plus the real byte size of each texture token, read out of computed styles rather than estimated. The only demo in the collection with JavaScript, and it exists because the numbers have to be yours, not mine.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
Banding is not a bug in your gradient, it is the 8-bit display pipeline showing through — and the fix is the one film and print have used for a century: add noise. A full-bleed dusk hero interpolated in oklab, stopped through the dark third, dithered at 3.5%, plus a side-by-side of the same ramp treated and untreated.
A contact sheet of three frames, each with a lens vignette under a luminosity-blended grain layer and its own grain weight. No canvas, no image processing, no filters on the photograph — four ordinary layers over an <img>. The port's neat trick: the per-frame weight that needed an inline style attribute is now an arbitrary-property utility, [--gtw08-weight:0.13].
Six product cards, identical except for one thing: grain weight tied to elevation. A card that sits higher catches more light and shows more tooth, so its noise is stronger — and the sixth card, with zero grain and the same fill, shadow and radius, is the one that looks like plastic. Four to eight percent opacity is the entire difference between a surface and a rectangle.
A production hero: unedited photograph, colour wash, two grain scales, and a legibility scrim — five stacked layers, zero added requests. The fine grain gives the surface tooth, the coarse grain at a fifth of the frequency gives the frame weather, and the staggered reveal is a single @theme animation token applied as animate-gtw10-rise.
Glass panels look synthetic because the blur averages everything underneath into smoothness — including any grain you put there. This demo is the A/B proof: same blur, same noise token, one panel with the grain behind the filter and one with it in front, then a real glass music player built the correct way.
A control kit where every surface is a material: buttons are raised, inputs are recessed, and both carry the same grain token. Press a button and three things change at once — grain weight, highlight direction and shadow depth. The port's wins are active:after: for the pressed texture and peer-user-invalid: for validation copy that cannot drift from the field it describes.
Three ways for two sections to meet like materials rather than like divs: a torn edge driven by feDisplacementMap, a masked grain fade, and a hairline with a grain wash spilling past it. The continuity trick is the same in all three — the grain layer runs across the join at identical weight, which is what makes two bands read as one surface.
A print halftone built from one radial-gradient tiled at a pitch, rotated to a screen angle, and blended over a high-contrast greyscale photograph. Three pitches on a specimen row, plus a two-ink duotone with its screens at 15° and 75° — thirty degrees apart, because that is what stops two dot grids from beating into moiré.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
Ordered dithering, the 1984 way: sixteen dispersed threshold values in a 4×4 tile, blended in hard-light over a deliberately banded gradient. Your eye reassembles the dot pattern into a smooth ramp. Three matrix sizes set three eras, and the same layer turns photographs into one-bit images.
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.
Risograph's charm is its failure mode: a paper feed that never lines up, so every colour lands a few millimetres off and the overlaps print as a third ink. Two offset ink layers, misregistered display type built from attr() pseudo-elements, and paper grain on top — four pixels of error, deliberately.
Four upholstery fabrics — linen, canvas, hessian, denim — built from two crossed repeating-linear-gradients, a highlight pass and a turbulence slub layer. Under 500 bytes each, tiling perfectly by construction, and fully recolourable, which is the version that actually works in a product configurator where the customer picks the colour.
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.
Animated grain is the single most likely thing in a texture collection to make somebody feel ill — rapid luminance change across a large area is exactly the trigger for vestibular disorders, migraine and photosensitivity. This demo animates a film gate properly (compositor transforms, steps(8)), and then designs the reduced-motion version instead of switching the animation off.
FAQ
Frequently asked questions
How do I generate grain in the browser without loading a texture image?
Every demo here builds noise from one SVG primitive rather than a PNG. You write an <svg> containing <filter> with a <feTurbulence> node, apply that filter to a <feFlood> or a full-bleed rect, then fold the whole document into a background-image: url('data:image/svg+xml,...'). That is the shippable form shown in Demo 03 — zero extra network requests, because the grain is markup, not a file. The two attributes that matter are type (use fractalNoise for cloudy film grain, turbulence for harder marble veins) and baseFrequency. Demo 01 breaks down every input; Demo 02 is a specimen sheet so you can see the size change. Keep the SVG tiny and let CSS background-repeat tile it, rather than sizing the SVG to the viewport.
What does baseFrequency actually control, and why does my grain look like blurry blobs?
baseFrequency is the spatial frequency of the noise — how many times the pattern repeats per user unit. Low values like 0.3 produce large soft cloud-like blobs; high values like 0.9 produce a tight, film-stock speckle. The blurry-blob bug is almost always a value that is too low for the density you wanted, so each noise cell spans many pixels. Demo 02 lays out the full range side by side so you can pick by eye instead of guessing. The second dial is numOctaves: it stacks additional layers of finer detail on top, so 1 is smooth and 3 or 4 adds crunch. More octaves cost more paint time, so raise it only until the texture reads right. A common combination for a photographic grain is a high baseFrequency with two or three octaves, then knock the whole layer's opacity down.
Why is my SVG noise coming out coloured instead of neutral grey grain?
Raw <feTurbulence> writes independent random values into the red, green and blue channels, so every pixel gets three unrelated numbers and the result is confetti, not grain. To get neutral monochrome speckle you have to collapse the channels after generation. Demo 05 does this with <feColorMatrix>: use type="matrix" and route all three input channels into equal luminance, or use type="saturate" with a value of 0 to strip colour entirely. Once it is grey you can re-tint deliberately by biasing the matrix toward a single hue — warm the grain for film, cool it for a print look. This is also why blending matters: a coloured raw-turbulence layer set to overlay will shift the hue of everything beneath it, which reads as a bug rather than a texture. Desaturate first, tint second.
How do I stop banding in a CSS gradient hero without switching to an image?
Banding is not a flaw in your gradient — it is the 8-bit display pipeline quantising a smooth ramp into visible steps, and it shows up worst on large dark-to-mid gradients. Demo 07 fixes it the cheap way: lay a very faint grain layer over the gradient so the noise dithers the boundary between adjacent bands, and the eye integrates it back into a smooth transition. You do not need much — grain at a few percent opacity is enough to break the hard step edges. The key is that the noise must sit above the gradient in stacking order and composite over it, typically with mix-blend-mode: overlay so it lightens and darkens symmetrically around mid-grey. This keeps the whole hero as pure CSS, so it stays crisp at any viewport width and adds no image payload, unlike exporting a pre-dithered PNG that then has to be sized for every breakpoint.
How expensive is an SVG grain filter to paint, and when will it hurt performance?
SVG filters are repainted by the CPU, not composited on the GPU like a transform, so a full-viewport <feTurbulence> that re-rasterises every frame is the case to avoid. Demo 06 measures this live rather than repeating folklore. The safe pattern is a static grain: generate the noise once into a data-URI background so the browser rasterises a single tile and caches it — that is nearly free after first paint. Cost climbs when the filter is applied to a large live element, when numOctaves is high, and worst of all when the grain is animated by cycling baseFrequency or a seed each frame, which forces a fresh full-size rasterisation continuously. Keep the generated SVG small and tile it with background-repeat; reserve animated grain for small elements, and gate it behind the reduced-motion fallback in Demo 20.
In what order do I stack a hero's photo, colour wash, grain and text so it stays legible?
Demo 10 is a production hero built as five explicit layers, bottom to top: the unedited photograph, a colour wash to unify the palette, a coarse grain, a fine grain, and a legibility scrim directly under the text. The order is the whole trick. The wash sits over the photo so the image reads as one tone rather than a busy snapshot. Grain goes over the wash — Demo 11 shows why: put grain under a backdrop-filter: blur() glass panel and the blur averages the speckle straight back into smoothness, so texture over glass must be the top layer, not a buried one. Finally the scrim — a dark gradient or a semi-opaque fill — goes above the grain but below the copy, because if grain lands on top of the text it fights the letterforms. Manage this with stacking context, giving each layer its own element rather than piling everything into one background shorthand.
Grain looks great in light mode but turns my dark UI muddy — how do I fix that?
This is the number-one reason teams rip texture back out of dark products, and Demo 19 is built specifically for it. The cause is the blend mode: overlay lightens values above mid-grey and darkens values below it, so on a dark surface almost every pixel is on the darken side and the grain only ever muddies the shadows — it never adds the highlight sparkle that makes light-mode grain feel tactile. The fix is to swap the composite. Use soft-light for a gentler symmetric effect, or on very dark panels use screen so the grain can only lighten, adding texture without deepening the murk. Pair that with a lower opacity than you would use on white, because dark backgrounds show noise contrast more readily. Test the same tile on both themes rather than assuming one blend mode carries across — the demos deliberately show the same grain failing and then working after the mode change.
Is animated film grain an accessibility problem, and what should I ship instead?
Yes — animated grain is the single most likely thing in a texture collection to trigger discomfort, because rapid frame-to-frame luminance flicker across a wide area is exactly the pattern that provokes motion sensitivity and, at the wrong frequency, seizure risk. Demo 20 is the required fallback: wrap any moving grain in @media (prefers-reduced-motion: reduce) and swap it for a static tile — the texture stays, only the motion stops. Practically, that means the animated seed or shifting baseFrequency is the thing you disable, while the generated noise background remains painted. There is a performance dividend too, since the static tile no longer re-rasterises every frame. Treat this as non-negotiable rather than a nicety: default to static grain everywhere, and only opt an element into animation when it is small and the reduced-motion branch is already in place. A texture is decorative, so it must never be the thing that makes the page unusable.