A flat fill reads as plastic. Textures are the family of surface effects that give the same hue a material: grain, relief, paper fiber, brushed specular. Normal maps are one 3D tool for lighting response — not the whole topic. For pure grain, see Noise.
Switch modes on the card below. Same base hue; only the surface changes.
Plastic is the wrong default. Grain breaks banding. Relief catches light. Paper fiber softens digital sheen. Brushed metal sells anisotropy with streaky specular — all without leaving CSS/SVG for most product chrome.
A family, not a filter
Pick the texture that matches the material you are implying:
- Grain / noise — film, soft UI cards, banded gradients
- Relief / bevel — inset panels, tactile chrome (pointer light optional)
- Paper / fiber — stationery, quiet editorial surfaces
- Brushed — metal, toolbars, hardware-adjacent UI
When the surface must light correctly under a moving 3D camera, leave CSS
and use real normal maps in Three.js / Filament. Do not purple-sludge the
specular. Keep highlights near white or a cool tint of the surface. Scarce
accent (#c0300f) belongs on actions, not on every lit edge.
Usage
<!-- Grain overlay (SVG filter defined once on the page) -->
<div class="relative isolate overflow-hidden rounded-xl bg-[#3a4556]">
<div
aria-hidden
class="pointer-events-none absolute inset-0 opacity-[0.14] mix-blend-overlay"
style="filter: url(#grain)"
></div>
</div>
<!-- Relief: inset bevel + movable specular -->
<div
class="h-32 rounded-xl"
style="
background:
radial-gradient(circle at 62% 28%, rgba(255,255,255,0.55), transparent 42%),
linear-gradient(145deg, #4a5568, #1e2633);
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.35),
inset 0 -1px 0 rgba(0,0,0,0.35);
"
></div>
/* Grain: empty overlay + SVG feTurbulence */
.grain-overlay {
position: absolute;
inset: 0;
opacity: 0.14;
mix-blend-mode: overlay;
filter: url(#grain);
pointer-events: none;
}
/* Relief: bevel + specular that follows --lx / --ly */
.relief {
background:
radial-gradient(
circle at var(--lx, 60%) var(--ly, 30%),
rgba(255, 255, 255, 0.55),
transparent 42%
),
linear-gradient(145deg, #4a5568, #1e2633);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.35),
inset 0 -1px 0 rgba(0, 0, 0, 0.35);
}
/* Paper fiber: fine horizontal lines */
.paper::after {
content: "";
position: absolute;
inset: 0;
opacity: 0.18;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 1px,
rgba(0, 0, 0, 0.08) 1px,
rgba(0, 0, 0, 0.08) 2px
);
}
Textures are a design choice, not a decoration pass. Match the surface to the object — then stop adding layers.