← Text effects

Boiling Line Text

The edges of the letters wobble non-stop, redrawn a few times a second like the “line boil” in a hand-drawn cartoon. A handful of noise filters take turns, so it’s nearly free to run.

Hand drawn

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-boiling-line-text to any heading. Outlines and cartoon shadows make the wobble easy to see.

Options

Add any of these attributes to the same element.

data-strengthHow far the edges wander, in em. Default 0.05.
data-detailHow many wobbles fit in each em: higher is crinklier. Default 2.5.
data-fpsHow many times a second it redraws. Default 9; cartoons use 8 to 12.
data-framesHow many different drawings it cycles through. Default 4.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Boiling Line Text: the edges of the letters wobble non-stop, redrawn a
    // few times a second like the "line boil" of a hand-drawn cartoon. A small
    // set of noise filters is swapped in turn, so it costs almost nothing.
    // Usage: <h1 data-boiling-line-text>Doodle</h1>
    // Optional: data-strength (how far the edges wander, in em; default
    // 0.05), data-detail (how many wobbles per em; default 2.5), data-fps
    // (redraws per second; default 9, cartoons use 8 to 12), data-frames
    // (how many different drawings it cycles through; default 4).
    var DEFAULTS = { strength: 0.05, detail: 2.5, fps: 9, frames: 4 };

    var count = 0;
    var holder;

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function svgEl(tag, attrs) {
      var n = document.createElementNS('http://www.w3.org/2000/svg', tag);
      for (var k in attrs) n.setAttribute(k, attrs[k]);
      return n;
    }

    function setup(el) {
      var d = el.dataset;
      var strength = d.strength !== undefined ? +d.strength : DEFAULTS.strength;
      var detail = +d.detail || DEFAULTS.detail;
      var fps = +d.fps || DEFAULTS.fps;
      var frames = Math.max(1, Math.min(10, +d.frames || DEFAULTS.frames));

      if (!holder) {
        holder = svgEl('svg', { width: 0, height: 0, 'aria-hidden': 'true', focusable: 'false' });
        holder.style.cssText = 'position:absolute;width:0;height:0;overflow:hidden';
        document.body.appendChild(holder);
      }
      var id = 'blt-' + (++count);
      var noises = [], maps = [];
      for (var i = 0; i < frames; i++) {
        var f = svgEl('filter', { id: id + '-' + i, x: '-10%', y: '-20%', width: '120%', height: '140%' });
        var noise = svgEl('feTurbulence', { type: 'fractalNoise', numOctaves: 2, seed: count * 13 + i * 7, result: 'n' });
        var map = svgEl('feDisplacementMap', { in: 'SourceGraphic', in2: 'n', xChannelSelector: 'R', yChannelSelector: 'G' });
        f.appendChild(noise);
        f.appendChild(map);
        holder.appendChild(f);
        noises.push(noise);
        maps.push(map);
      }

      // Wobble sizes follow the font size, so big and small text boil alike.
      function size() {
        var fs = parseFloat(getComputedStyle(el).fontSize);
        noises.forEach(function (n) { n.setAttribute('baseFrequency', (detail / fs).toFixed(4)); });
        maps.forEach(function (m) { m.setAttribute('scale', (strength * fs * 2).toFixed(2)); });
      }
      size();
      new ResizeObserver(size).observe(el);

      var frame = 0;
      function show(i) { el.style.filter = 'url(#' + id + '-' + i + ')'; }
      show(0);
      if (reduced()) return;

      var acc = 0, prev = 0;
      var ticker = anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          acc += prev ? Math.min(200, now - prev) : 0;
          prev = now;
          if (acc < 1000 / fps) return;
          acc = 0;
          frame = (frame + 1) % frames;
          show(frame);
        }
      });

      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { prev = 0; ticker.play(); }
        else ticker.pause();
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-boiling-line-text]').forEach(function (el) {
        if (el._blt) return;
        el._blt = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h1 data-boiling-line-text>Hand drawn</h1>

<!-- shakier and faster: -->
<h1 data-boiling-line-text data-strength="0.08" data-fps="12">Jittery</h1>