The browser is a generative design tool. Most people don’t use it that way.
Generative design — systems that produce visual output through rules and randomness — is usually associated with Processing, p5.js, or dedicated tools like Houdini (the 3D software, not the CSS spec). But the browser has had powerful generative primitives built in for years. SVG filters, CSS custom properties, trigonometric functions, and the <canvas> API together form a complete toolkit for math-driven visual work.
No libraries. No build step. Just the platform.
SVG filters
The thing that took me longest to notice about SVG filters: the filter CSS property accepts a URL reference to an SVG <filter> definition, and that filter runs on any element — not just SVG content. You can define a noise texture in an invisible <svg> block and apply it via CSS to a <div>, a <section>, a heading. The effect pipeline runs on the rendered pixels, completely separate from the DOM structure.
The basic syntax:
<svg style="display: none">
<defs>
<filter id="noise">
<feTurbulence
type="fractalNoise"
baseFrequency="0.65"
numOctaves="3"
stitchTiles="stitch"
/>
<feColorMatrix type="saturate" values="0" />
</filter>
</defs>
</svg>
Apply it to any element:
.textured {
filter: url(#noise);
}
That’s a grayscale noise texture applied directly in CSS. No image file, no canvas, no JavaScript.
feTurbulence — sculpting noise
feTurbulence is the primitive that generates Perlin noise and fractal noise patterns. It’s the foundation of most SVG-based generative work.
The key parameters:
<feTurbulence
type="fractalNoise"
baseFrequency="0.04 0.08"
numOctaves="4"
seed="2"
stitchTiles="stitch"
/>
type—turbulencegives sharp, cloudy patterns;fractalNoisegives smoother organic texturesbaseFrequency— controls the scale of the noise. Lower values = larger, smoother shapes. Two values set x and y independently, which creates directional grainnumOctaves— layers of detail. More octaves = more complexity, more processingseed— the random seed. Change this to get a completely different pattern with the same settings
Think of baseFrequency as a zoom level and numOctaves as a detail slider. You’re sculpting a noise field.
Animating feTurbulence
CSS can’t animate SVG filter attributes directly — but JavaScript can, and requestAnimationFrame makes it smooth:
const turbulence = document.querySelector('feTurbulence');
let frame = 0;
function animate() {
frame++;
const freq = 0.04 + Math.sin(frame * 0.01) * 0.02;
turbulence.setAttribute('baseFrequency', freq);
requestAnimationFrame(animate);
}
animate();
The noise field breathes. The frequency oscillates, creating a slow, organic shift in the texture.
Building a noise texture with feDisplacementMap
Displacement mapping moves pixels based on a source image — in this case, the noise from feTurbulence. It’s how you get that melting, warping, liquid effect:
<filter id="displace">
<feTurbulence
type="turbulence"
baseFrequency="0.015"
numOctaves="2"
result="noise"
/>
<feDisplacementMap
in="SourceGraphic"
in2="noise"
scale="40"
xChannelSelector="R"
yChannelSelector="G"
/>
</filter>
The scale attribute controls the intensity of the distortion. At scale="40", text warps dramatically. At scale="5", it’s a subtle organic wobble. Apply it to text and you get type that looks hand-drawn or printed on uneven paper.
CSS trigonometry for math-driven layouts
CSS now has sin(), cos(), tan(), atan2(), and sqrt(). These aren’t just curiosities — they enable layout patterns that were previously only possible with JavaScript.
A circle of elements without JavaScript:
.orbit {
position: relative;
width: 300px;
height: 300px;
}
.dot {
position: absolute;
--angle: calc(var(--i) * (360deg / var(--total)));
--radius: 120px;
top: calc(50% + sin(var(--angle)) * var(--radius));
left: calc(50% + cos(var(--angle)) * var(--radius));
transform: translate(-50%, -50%);
}
Set --i and --total on each element via inline style or nth-child, and they distribute themselves around a circle. No JavaScript, no canvas, no SVG — pure CSS geometry.
@property for animated generative values
@property lets you define typed CSS custom properties that the browser can interpolate. This unlocks animation of values that were previously static:
@property --angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
@property --hue {
syntax: '<number>';
inherits: false;
initial-value: 0;
}
.rotating-gradient {
background: conic-gradient(
from var(--angle),
hsl(var(--hue), 70%, 50%),
hsl(calc(var(--hue) + 120), 70%, 50%),
hsl(calc(var(--hue) + 240), 70%, 50%)
);
animation: spin 4s linear infinite;
}
@keyframes spin {
to { --angle: 360deg; }
}
A smoothly rotating conic gradient. The browser interpolates --angle as a proper angle type, not as a string — which is why the transition is smooth.
Combining everything: a generative hero
Here’s how these primitives combine into something real — a generative hero section with a noise texture, a displaced gradient, and a math-positioned particle field:
.hero {
background:
linear-gradient(135deg,
hsl(var(--hue-1), 70%, 20%),
hsl(var(--hue-2), 60%, 10%)
);
filter: url(#noise-texture);
position: relative;
overflow: hidden;
}
<svg style="display: none">
<defs>
<filter id="noise-texture" x="0" y="0" width="100%" height="100%"
color-interpolation-filters="sRGB">
<feTurbulence
type="fractalNoise"
baseFrequency="0.65"
numOctaves="3"
stitchTiles="stitch"
result="noise"
/>
<feBlend in="SourceGraphic" in2="noise" mode="overlay" result="blend" />
<feComposite in="blend" in2="SourceGraphic" operator="in" />
</filter>
</defs>
</svg>
The feBlend with mode="overlay" composites the noise onto the source graphic instead of replacing it, preserving the underlying gradient while adding texture.
The generative mindset
The shift from using these tools to thinking with them is subtle but important.
When you use a noise filter, you’re applying a texture. When you think with noise, you start asking: what if this value responded to scroll position? What if the seed changed on hover? What if the baseFrequency was driven by audio input?
Generative design isn’t a visual style — it’s a method. The browser gives you the primitives. What you build with them is up to you.