Daring Designs uses cookies to enhance your experience, analyze site traffic, and improve our services. By clicking dismiss, you agree to our use of cookies.

Skip to main content
Daring Designs
Contact Search

Warping elements with
feTurbulence and feDisplacementMap.

How to warp, ripple, and distort HTML elements with SVG filters, with copy-paste code, and the one gotcha that will waste your afternoon.

I was building a new design called Illume and wanted a heat-haze, watery-glass warp on some elements, the kind of organic distortion you normally reach for a shader or a canvas for. It turns out the browser has shipped this since forever, buried in SVG filters. Two primitives do all the work: feTurbulence generates procedural noise, and feDisplacementMap uses that noise to shove pixels around. Chain them and you get ripples, gooey blobs, torn paper edges, and heat shimmer, all in a few lines of markup and CSS. No WebGL, no JavaScript required.

Want to skip straight to playing with it? I built a free client-side SVG Warp tool where you can upload your own SVG, drag the sliders, click individual shapes to target them, and download the result. Everything below explains what those knobs actually do.

The Two Primitives

feTurbulence procedurally generates Perlin (fractal) noise. You don't feed it an image, it synthesizes a noise texture from a seed and a frequency. On its own it just looks like colored static, or clouds if you turn the frequency down.

feDisplacementMap takes two inputs: the graphic you want to distort, and a second image whose pixel values are read as a displacement field. For each pixel, it looks up the corresponding pixel in the map, reads a color channel, and uses that value to offset where it samples from the source. Point it at the turbulence output and every pixel gets nudged by the noise. That is the whole trick.

A Minimal Warp

Here is the smallest useful example. Define a filter in an inline SVG, then reference it from CSS with filter: url(#id). This warps a plain div and its text:

<svg width="0" height="0" style="position:absolute">
  <filter id="warp">
    <feTurbulence
      type="fractalNoise"
      baseFrequency="0.02"
      numOctaves="3"
      seed="7"
      result="noise"
    />
    <feDisplacementMap
      in="SourceGraphic"
      in2="noise"
      scale="30"
      xChannelSelector="R"
      yChannelSelector="G"
    />
  </filter>
</svg>

<div class="warped">
  <h2>Illume</h2>
  <p>This whole block is being displaced by procedural noise.</p>
</div>
.warped {
  filter: url(#warp);
}

That is a complete, working effect. The knobs that matter:

  • baseFrequency controls the scale of the noise. Small values (0.01 to 0.03) give slow, rolling, watery waves. Large values (0.5+) give tight, sandpaper-fine distortion. You can pass two numbers for different X and Y frequencies, e.g. "0.01 0.04" for a stretched, directional ripple.

  • scale on the displacement map is the amplitude, how far pixels can move, in pixels. This is the single biggest visual lever. 0 is no effect, 30 is a heavy warp, 100 is nearly unreadable soup. Animate this from 0 to fade the effect in.

  • numOctaves stacks layers of noise at increasing frequencies for more natural, detailed turbulence. 1 is smooth, 4+ is rich but more expensive to compute.

  • xChannelSelector / yChannelSelector pick which color channel of the noise drives horizontal vs vertical displacement. Using R for X and G for Y keeps them independent so the warp does not just shear diagonally.

The Gotcha: It Will Not Work Inside an <img> Tag

Here is the thing that cost me an afternoon. You cannot apply this filter directly to an <img> element and get a warped photo, at least not reliably. This looks like it should work and does nothing useful:

<!-- Looks fine. Warps nothing. -->
<img src="hero.jpg" style="filter: url(#warp)" alt="">

The displacement primitive needs to sample pixels from around each source pixel, including transparent or out-of-bounds areas. A raster <img> is treated as opaque content with a hard edge, and the filter region clips against it in ways that either produce nothing, a cropped mess, or a black-fringed border. Different engines disagree on exactly how badly. The practical upshot is the same everywhere: displacing a bare <img> is not a supported, portable technique.

The reliable fix is to stop using <img> and paint the image as a background instead. Put it on a div (or an SVG <image> element inside the filter graph), and the filter has a proper rectangular region to work with:

<!-- Warps correctly. -->
<div class="photo" style="filter: url(#warp)"></div>
.photo {
  width: 600px;
  height: 400px;
  background-image: url("hero.jpg");
  background-size: cover;
}

If you specifically need the image inside the SVG (so you can composite it with other filter primitives), reference it as an <feImage> and feed that into feDisplacementMap instead of SourceGraphic:

<svg width="600" height="400">
  <filter id="warp-image" x="-20%" y="-20%" width="140%" height="140%">
    <feImage href="hero.jpg" result="pic" />
    <feTurbulence
      type="fractalNoise"
      baseFrequency="0.015"
      numOctaves="2"
      result="noise"
    />
    <feDisplacementMap
      in="pic"
      in2="noise"
      scale="25"
      xChannelSelector="R"
      yChannelSelector="G"
    />
  </filter>
  <rect width="600" height="400" filter="url(#warp-image)" />
</svg>

Note the expanded filter region, x="-20%" width="140%". By default a filter is clipped to a box slightly larger than the element, and a heavy displacement will push pixels right past that edge and get them chopped off. Growing the region gives the warp room to breathe. This is worth doing on the CSS version too if you see hard clipping at the edges.

Animating It: Heat Haze

A static warp is nice, but the effect really comes alive when it moves. You animate the baseFrequency (or the seed) over time and the noise field crawls, giving you shimmering heat haze or flowing water. SVG has a native <animate> element for this, no JavaScript:

<svg width="0" height="0" style="position:absolute">
  <filter id="haze">
    <feTurbulence
      type="fractalNoise"
      baseFrequency="0.01 0.03"
      numOctaves="2"
      result="noise"
    >
      <animate
        attributeName="baseFrequency"
        dur="12s"
        values="0.01 0.03; 0.012 0.024; 0.01 0.03"
        repeatCount="indefinite"
      />
    </feTurbulence>
    <feDisplacementMap
      in="SourceGraphic"
      in2="noise"
      scale="18"
      xChannelSelector="R"
      yChannelSelector="G"
    />
  </filter>
</svg>

Keep the animation slow (10s+) and the values close together. Big or fast jumps in baseFrequency read as a jarring boil rather than a gentle shimmer. This is GPU-friendly enough for a hero, but do not slap it on a dozen elements at once, filter regions get re-rasterized every frame and it adds up fast.

Animating It: Interactive Ripple

For an interactive version, drive the scale from JavaScript so the element settles to calm and warps on hover or pointer movement. Grab the feDisplacementMap node and write to its scale attribute:

<svg width="0" height="0" style="position:absolute">
  <filter id="ripple">
    <feTurbulence
      type="fractalNoise"
      baseFrequency="0.02"
      numOctaves="3"
      seed="2"
      result="noise"
    />
    <feDisplacementMap
      id="rippleDisplace"
      in="SourceGraphic"
      in2="noise"
      scale="0"
      xChannelSelector="R"
      yChannelSelector="G"
    />
  </filter>
</svg>

<div class="card" style="filter: url(#ripple)">
  <h3>Hover me</h3>
</div>
const card = document.querySelector('.card');
const displace = document.getElementById('rippleDisplace');

let current = 0;
let target = 0;

card.addEventListener('mouseenter', () => { target = 24; });
card.addEventListener('mouseleave', () => { target = 0; });

function tick() {
  // Ease current toward target so it warps and settles smoothly.
  current += (target - current) * 0.1;
  displace.setAttribute('scale', current.toFixed(2));
  requestAnimationFrame(tick);
}
tick();

Easing the scale value toward a target (rather than snapping it) is what makes it feel like liquid instead of a light switch. You can drive target from pointer velocity, scroll position, or an audio level just as easily.

Bonus: The Gooey Trick

A close cousin worth knowing: combine feGaussianBlur with feColorMatrix to make overlapping shapes merge like blobs of mercury. Blur everything, then crank the alpha contrast so soft edges snap back to hard ones, and touching elements fuse:

<svg width="0" height="0" style="position:absolute">
  <filter id="goo">
    <feGaussianBlur in="SourceGraphic" stdDeviation="10" result="blur" />
    <feColorMatrix
      in="blur"
      mode="matrix"
      values="1 0 0 0 0
              0 1 0 0 0
              0 0 1 0 0
              0 0 0 20 -10"
      result="goo"
    />
    <feComposite in="SourceGraphic" in2="goo" operator="atop" />
  </filter>
</svg>

Apply filter: url(#goo) to a container and animate some circles inside it; where they overlap they will bleed together into a single organic shape. It is the same family of technique, procedural pixel manipulation entirely in the filter graph, and it pairs beautifully with a displacement warp layered on top.

Performance and Gotchas Recap

  • Not on bare <img>: paint the image as a background on a div, or pull it into the filter graph with <feImage>.

  • Expand the filter region with x/y/width/height (or CSS equivalents) so heavy displacement is not clipped at the edges.

  • Animate scale, not everything: animating baseFrequency re-generates the entire noise texture each frame and is the expensive path. Prefer animating the displacement scale for interaction, and reserve animated turbulence for one slow hero effect.

  • Text stays selectable: because you are filtering real DOM, the warped text is still real text, still selectable, still accessible. That is a genuine advantage over baking the effect into an image.

  • Respect reduced motion: gate the animated variants behind a prefers-reduced-motion: no-preference media query. A constantly shimmering page is genuinely nauseating for some people.

feTurbulence and feDisplacementMap have been sitting in the platform for years, quietly capable of effects most people assume require a canvas or a shader. Once you internalize that the noise is just a displacement field and the whole thing is one filter graph, a lot of expensive-looking motion design collapses into a handful of declarative lines. Just don't put it on an <img>.