← Seasonal

Pumpkin Flicker

A carved jack-o’-lantern glows from a candle inside. The light wavers, dips and flares like a real flame, and a warm glow pulses round the pumpkin and onto the ground.

How to use it

  1. Paste the snippet just before </head>.
  2. Add an empty <div data-pumpkin-flicker> where the pumpkin should sit. It looks best on a dark background.

Options

Add any of these attributes to the same element.

data-facegrin or spooky. Default grin.
data-speedFlicker speed multiplier. Default 1.
data-glowColour of the candlelight. Default #fde047.

It is 8:5 with a transparent background by default; set your own height or background on the element. It pauses when off screen. With reduced motion the face glows steadily.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Pumpkin Flicker: a carved jack-o'-lantern whose face glows from a candle
    // inside. The light wavers, dips and flares like a real flame, and a warm
    // glow pulses round the pumpkin and onto the ground. Drawn as SVG that
    // fills the element, and paused when off screen.
    // Usage: <div data-pumpkin-flicker></div>
    // It is 8:5 by default with a transparent background; it looks best on
    // something dark.
    // Optional: data-face ("grin", default, or "spooky"), data-speed (flicker
    // speed multiplier, default 1), data-glow (light colour, default #fde047).
    var DEFAULTS = { face: 'grin', speed: 1, glow: '#fde047' };
    var uid = 0;

    var css =
      '[data-pumpkin-flicker]{display:block;position:relative;width:100%;aspect-ratio:8/5}' +
      '.pkf-svg{position:absolute;inset:0;width:100%;height:100%;display:block;overflow:visible}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    // Face cut-outs, in viewBox units, centred on the pumpkin at (200, 148).
    var FACES = {
      grin:
        'M160 126 L178 104 L190 130 Z' +
        'M240 126 L222 104 L210 130 Z' +
        'M200 134 L192 150 L208 150 Z' +
        'M150 156 Q200 196 250 156 L240 160 L234 154 L226 166 L216 160 L208 172 L200 162 L192 172 L184 160 L174 166 L166 154 L160 160 Z',
      spooky:
        'M156 130 Q170 104 190 124 Q176 120 170 134 Z' +
        'M244 130 Q230 104 210 124 Q224 120 230 134 Z' +
        'M194 138 L200 150 L206 138 Z' +
        'M156 170 L168 158 L176 172 L186 160 L194 174 L200 162 L206 174 L214 160 L224 172 L232 158 L244 170 Q200 190 156 170 Z'
    };

    function build(el) {
      var d = el.dataset;
      var n = ++uid;
      var face = FACES[d.face] || FACES[DEFAULTS.face];
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      var light = d.glow || DEFAULTS.glow;
      if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', 'A carved pumpkin glowing with candlelight');
      if (!el.hasAttribute('role')) el.setAttribute('role', 'img');

      var g = 'pkf' + n;
      var rib = function (cx, rx, ry) {
        return '<ellipse cx="' + cx + '" cy="150" rx="' + rx + '" ry="' + ry + '" fill="url(#' + g + '-skin)" stroke="#9a3412" stroke-opacity=".45" stroke-width="1.5"/>';
      };
      el.innerHTML =
        '<svg class="pkf-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
        '<defs>' +
        '<radialGradient id="' + g + '-halo"><stop offset="0" stop-color="#fb923c" stop-opacity=".5"/><stop offset=".5" stop-color="#ea580c" stop-opacity=".14"/><stop offset="1" stop-color="#ea580c" stop-opacity="0"/></radialGradient>' +
        '<radialGradient id="' + g + '-pool"><stop offset="0" stop-color="#fdba74" stop-opacity=".6"/><stop offset="1" stop-color="#f97316" stop-opacity="0"/></radialGradient>' +
        '<radialGradient id="' + g + '-skin" cx=".4" cy=".35" r=".75"><stop offset="0" stop-color="#fb923c"/><stop offset=".6" stop-color="#ea580c"/><stop offset="1" stop-color="#9a3412"/></radialGradient>' +
        '<radialGradient id="' + g + '-fire" cx=".5" cy=".62" r=".6"><stop offset="0" stop-color="#fffbe6"/><stop offset=".55" stop-color="' + light + '"/><stop offset="1" stop-color="#facc15"/></radialGradient>' +
        '<radialGradient id="' + g + '-face" cx=".5" cy=".6" r=".55"><stop offset="0" stop-color="#fde68a" stop-opacity=".55"/><stop offset="1" stop-color="#fde68a" stop-opacity="0"/></radialGradient>' +
        '<filter id="' + g + '-blur" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="3.5"/></filter>' +
        '<clipPath id="' + g + '-cut"><path d="' + face + '"/></clipPath>' +
        '</defs>' +
        '<circle class="pkf-halo" cx="200" cy="150" r="170" fill="url(#' + g + '-halo)"/>' +
        '<ellipse class="pkf-pool" cx="200" cy="210" rx="130" ry="16" fill="url(#' + g + '-pool)"/>' +
        '<ellipse cx="200" cy="210" rx="86" ry="8" fill="#000" opacity=".35"/>' +
        // Stem.
        '<path d="M194 94 Q192 76 186 68 Q196 62 206 70 Q204 80 208 94 Z" fill="#4d7c0f"/>' +
        '<path d="M199 92 Q198 78 194 70" stroke="#365314" stroke-width="2" fill="none"/>' +
        // Ribs, back to front, then a soft sheen.
        rib(146, 44, 58) + rib(254, 44, 58) +
        rib(166, 46, 62) + rib(234, 46, 62) +
        rib(200, 50, 64) +
        '<ellipse class="pkf-skinglow" cx="200" cy="160" rx="80" ry="48" fill="url(#' + g + '-face)"/>' +
        // The face: a dark rim (the cut flesh), then the candlelit inside.
        '<path d="' + face + '" fill="#7c2d12" transform="translate(-1.5 -2)"/>' +
        '<g clip-path="url(#' + g + '-cut)">' +
          '<rect x="140" y="96" width="120" height="96" fill="#ea580c"/>' +
          '<rect class="pkf-fire" x="140" y="96" width="120" height="96" fill="url(#' + g + '-fire)"/>' +
          '<path class="pkf-shadow" d="' + face + '" fill="none" stroke="#9a3412" stroke-width="3" opacity=".55" transform="translate(1 1.5)"/>' +
        '</g>' +
        // Light spilling out of the holes.
        '<path class="pkf-spill" d="' + face + '" fill="' + light + '" filter="url(#' + g + '-blur)" opacity="0"/>' +
        '</svg>';

      var svg = el.firstChild;
      var fire = svg.querySelector('.pkf-fire');
      var halo = svg.querySelector('.pkf-halo');
      var pool = svg.querySelector('.pkf-pool');
      var skin = svg.querySelector('.pkf-skinglow');
      var spill = svg.querySelector('.pkf-spill');

      // Candle flicker: layered sines for the steady waver, plus random dips
      // and flares that ease in and out.
      var t = 0, gust = 0, gustTarget = 0, nextGust = 600;
      function level() {
        var s = t / 1000;
        var base = 0.86 + 0.08 * Math.sin(s * 7.3) + 0.06 * Math.sin(s * 13.1 + 1.7) + 0.04 * Math.sin(s * 23.7 + 0.4);
        return Math.max(0.4, Math.min(1, base + gust));
      }
      function frame(dt) {
        t += dt * speed;
        nextGust -= dt * speed;
        if (nextGust <= 0) {
          gustTarget = Math.random() < 0.65 ? -(0.2 + Math.random() * 0.35) : 0.12 + Math.random() * 0.1;
          nextGust = 250 + Math.random() * 1400;
        }
        gust += (gustTarget - gust) * Math.min(1, dt / 90);
        gustTarget *= Math.pow(0.5, dt / 220);
        var v = level();
        fire.setAttribute('opacity', Math.min(1, 0.3 + v * 0.8).toFixed(3));
        halo.setAttribute('opacity', (0.35 + v * 0.65).toFixed(3));
        pool.setAttribute('opacity', (v * v).toFixed(3));
        skin.setAttribute('opacity', (v * 0.9).toFixed(3));
        spill.setAttribute('opacity', (Math.pow(v, 2) * 0.55).toFixed(3));
        // The light leans a touch as the flame sways.
        fire.setAttribute('transform', 'translate(' + (Math.sin(t / 340) * 4).toFixed(2) + ' ' + ((1 - v) * 6).toFixed(2) + ')');
      }

      if (reduced()) {
        frame(0);
        return;
      }

      frame(0);
      var last = 0;
      var timer = anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          var dt = last ? Math.min(50, now - last) : 16;
          last = now;
          frame(dt);
        }
      });
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-pumpkin-flicker]').forEach(function (el) {
        if (el._pkf) return;   // idempotent
        el._pkf = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-pumpkin-flicker style="background: #120a1f"></div>

<!-- spooky face, eerie green light: -->
<div data-pumpkin-flicker data-face="spooky" data-glow="#bef264"></div>