Copied!
Navigation and lists

Wiggle drop header

A site header that slides away as you scroll down and drops back in with an elastic wobble along its bottom edge.

← Library

Element · Vanilla JS + SVGUpdated November 6, 2026

ScrollElementVanilla JS + SVG

Live preview needs JavaScript. The code below works without any dependencies.

Prompt for your AI assistant

Paste this into Claude, Cursor or ChatGPT. The settings update as you move the controls above.

Build "Wiggle drop header" for my site using the Enrichr Library element at https://enrichr.dev/elements/wiggle-drop-header.html.

Read the machine-readable spec and source at https://enrichr.dev/elements/wiggle-drop-header.json (markup, CSS, JavaScript, parameters).
Requirements: vanilla JavaScript and CSS, no new dependencies; keep the behaviour of the source unchanged; respect prefers-reduced-motion; work on touch screens; keep it accessible.

Settings:
- autoplay: on
- wiggle: 240
- speed: 1.8
- distortion: 0
- pause: 1.5
- links: Library, Tools, Pricing
- cta: Get started
- colour1: #fbf8f1
- colour2: #e6ff55
- ink: #0c1e18
- background: #0c1e18
- text: #fbf8f1

Verify: reduced motion shows no movement, it works at 375px wide, there are no console errors, and scrolling stays smooth.

The same spec as JSON, for tools that read it directly: /elements/wiggle-drop-header.json.

Code

Mount it on an element that contains the markup. It returns an object with update(options) and destroy().

HTML

<div class="ef-wdh">
  <header class="ef-wdh__bar">
    <div class="ef-wdh__shape" aria-hidden="true">
      <svg class="ef-wdh__svg" preserveAspectRatio="none" viewBox="0 0 2278 240" focusable="false">
        <defs>
          <linearGradient class="ef-wdh__grad" id="ef-wdh-grad" x1="0" y1="0" x2="2278" y2="240" gradientUnits="userSpaceOnUse">
            <stop class="ef-wdh__s1" offset="0.2" stop-color="#fbf8f1"></stop>
            <stop class="ef-wdh__s2" offset="0.8" stop-color="#e6ff55"></stop>
          </linearGradient>
          <filter class="ef-wdh__filter" id="ef-wdh-dist" x="-20%" y="-20%" width="140%" height="140%">
            <feTurbulence class="ef-wdh__turb" type="fractalNoise" baseFrequency="0.004" numOctaves="1" result="rawNoise"></feTurbulence>
            <feGaussianBlur in="rawNoise" stdDeviation="2" result="noise"></feGaussianBlur>
            <feDisplacementMap class="ef-wdh__disp" in="SourceGraphic" in2="noise" scale="0" xChannelSelector="R" yChannelSelector="G"></feDisplacementMap>
          </filter>
        </defs>
        <g class="ef-wdh__g"><path class="ef-wdh__path" fill="url(#ef-wdh-grad)" d="M0,0 L2278,0 L2278,80 C2278,80, 1814,80, 1139,80 C464,80, 0,80, 0,80 Z"></path></g>
      </svg>
    </div>
    <div class="ef-wdh__nav">
      <a class="ef-wdh__logo" href="#" aria-label="Enrichr home"><svg class="ef-wdh__mark" viewBox="0 0 50 50" aria-hidden="true" focusable="false"><path fill="currentColor" d="m34.213 25.361 c0 2.655 1.381 4.808 3.086 4.808 0.781 0 1.494 -0.452 2.038 -1.198 0.267 -0.366 0.688 -0.589 1.142 -0.589 h0.054 c0.774 0 1.401 0.627 1.401 1.401 v10.65 h-10.651 c-0.774 0 -1.401 0.627 -1.401 1.401 v0.054 c0 0.453 0.222 0.874 0.589 1.141 0.746 0.544 1.198 1.257 1.198 2.038 0 1.705 -2.152 3.086 -4.808 3.086 -2.655 0 -4.808 -1.381 -4.808 -3.086 0 -0.781 0.452 -1.494 1.198 -2.038 0.366 -0.267 0.589 -0.688 0.589 -1.142 v-0.054 c0 -0.774 -0.627 -1.401 -1.401 -1.401 h-10.65 v-10.651 c0 -0.774 -0.627 -1.401 -1.401 -1.401 h-0.054 c-0.453 0 -0.875 0.222 -1.142 0.589 -0.543 0.746 -1.257 1.198 -2.038 1.198 -1.705 0 -3.086 -2.152 -3.086 -4.808 0 -2.655 1.381 -4.808 3.086 -4.808 0.781 0 1.495 0.452 2.038 1.198 0.267 0.366 0.688 0.589 1.141 0.589 h0.054 c0.774 0 1.401 -0.627 1.401 -1.401 v-10.65 h10.65 c0.774 0 1.401 -0.627 1.401 -1.401 v-0.054 c0 -0.453 -0.222 -0.875 -0.589 -1.142 -0.746 -0.543 -1.198 -1.257 -1.198 -2.038 0 -1.705 2.152 -3.086 4.808 -3.086 2.655 0 4.808 1.381 4.808 3.086 0 0.781 -0.452 1.495 -1.198 2.038 -0.366 0.267 -0.589 0.688 -0.589 1.141 v0.054 c0 0.774 0.627 1.401 1.401 1.401 h10.65 v10.65 c0 0.774 -0.627 1.401 -1.401 1.401 h-0.053 c-0.453 0 -0.874 -0.222 -1.141 -0.589 -0.544 -0.746 -1.257 -1.198 -2.038 -1.198 -1.705 0 -3.086 2.152 -3.086 4.808 z"/></svg><span class="ef-wdh__word">Enrichr</span></a>
      <nav class="ef-wdh__links" aria-label="Main"></nav>
      <a class="ef-wdh__cta" href="#"></a>
    </div>
  </header>
  <div class="ef-wdh__main">
    <h2 class="ef-wdh__h">Scroll to wiggle.</h2>
    <p class="ef-wdh__p"></p>
    <button type="button" class="ef-wdh__test">Test animation</button>
  </div>
</div>

CSS

/* Enrichr Library · https://enrichr.dev */
.ef-wdh { --ef-wdh-bg: #0c1e18; --ef-wdh-text: #fbf8f1; --ef-wdh-ink: #0c1e18; position: relative; width: 100%; min-height: 26rem; box-sizing: border-box; overflow: hidden; border-radius: 1rem; background: var(--ef-wdh-bg); color: var(--ef-wdh-text); font-family: 'Instrument Sans', system-ui, sans-serif; container-type: inline-size; }
.ef-wdh .ef-wdh__bar { position: absolute; top: 0; left: 0; z-index: 2; width: 100%; height: 80px; will-change: transform; }
.ef-wdh .ef-wdh__shape { position: absolute; top: 0; left: 0; z-index: -1; width: 100%; height: 240px; pointer-events: none; }
.ef-wdh .ef-wdh__svg { display: block; width: 100%; height: 100%; overflow: visible; }
.ef-wdh .ef-wdh__nav { box-sizing: border-box; max-width: 1200px; height: 80px; margin: 0 auto; padding: 0 24px; display: flex; align-items: center; justify-content: space-between; gap: 1rem; color: var(--ef-wdh-ink); }
.ef-wdh .ef-wdh__logo { display: flex; align-items: center; gap: 0.35rem; color: inherit; text-decoration: none; font-family: 'Parkinsans', 'Space Grotesk', sans-serif; font-size: 1.5rem; font-weight: 800; line-height: 1; letter-spacing: -0.05em; }
.ef-wdh .ef-wdh__mark { width: 2rem; height: 2rem; flex: none; }
.ef-wdh .ef-wdh__links { display: none; gap: 32px; }
.ef-wdh .ef-wdh__link { color: inherit; text-decoration: none; font-family: 'Parkinsans', 'Space Grotesk', sans-serif; font-size: 1rem; font-weight: 600; transition: color 0.3s; }
.ef-wdh .ef-wdh__link:hover { color: #126e45; }
.ef-wdh .ef-wdh__cta { flex: none; padding: 10px 24px; border: none; border-radius: 9999px; background: var(--ef-wdh-ink); color: var(--ef-wdh-text); font-family: 'Parkinsans', 'Space Grotesk', sans-serif; font-size: 0.875rem; font-weight: 700; line-height: 1.2; text-decoration: none; cursor: pointer; transition: all 0.3s; }
.ef-wdh .ef-wdh__cta:hover { background: var(--ef-wdh-text); color: var(--ef-wdh-ink); }
.ef-wdh .ef-wdh__logo:focus-visible, .ef-wdh .ef-wdh__link:focus-visible, .ef-wdh .ef-wdh__cta:focus-visible { outline: 2px solid var(--ef-wdh-ink); outline-offset: 4px; border-radius: 6px; }
.ef-wdh .ef-wdh__cta:focus-visible { border-radius: 9999px; }
.ef-wdh .ef-wdh__main { position: relative; z-index: 1; box-sizing: border-box; min-height: 26rem; padding: 10rem 24px 2.5rem; display: flex; flex-direction: column; align-items: center; text-align: center; }
.ef-wdh .ef-wdh__h { margin: 0; font-family: 'Parkinsans', 'Space Grotesk', sans-serif; font-size: clamp(2rem, 6cqi, 3rem); font-weight: 800; line-height: 1.1; letter-spacing: -0.03em; color: inherit; opacity: 1; }
.ef-wdh .ef-wdh__p { max-width: 34rem; margin: 1rem 0 0; font-size: 1rem; line-height: 1.5; color: color-mix(in srgb, var(--ef-wdh-text) 62%, var(--ef-wdh-bg)); opacity: 1; }
.ef-wdh .ef-wdh__test { margin-top: 1.6rem; padding: 10px 24px; border: 1px solid color-mix(in srgb, var(--ef-wdh-text) 35%, transparent); border-radius: 9999px; background: transparent; color: inherit; font-family: 'Parkinsans', 'Space Grotesk', sans-serif; font-size: 0.875rem; font-weight: 700; cursor: pointer; transition: all 0.3s; }
.ef-wdh .ef-wdh__test:hover { background: var(--ef-wdh-text); color: var(--ef-wdh-bg); }
.ef-wdh .ef-wdh__test:focus-visible { outline: 2px solid #e6ff55; outline-offset: 3px; }
.ef-wdh.is-still .ef-wdh__bar { transition: opacity 0.2s ease; }
.ef-wdh.is-still.is-off .ef-wdh__bar { opacity: 0; }
@container (min-width: 640px) { .ef-wdh .ef-wdh__links { display: flex; } }

JavaScript

// Enrichr Library · https://enrichr.dev
// Wiggle drop header: a site header that slides up out of view and drops back in with a wobble along its bottom edge.
// The header's colour is one SVG path. To drop it in, the middle of the path's bottom edge is pulled down to the wiggle
// amount at once, then springs back to a straight line on an elastic out curve (amplitude 1.2, period 0.3) over the drop
// duration, while the header slides down from -100% in 44% of that time on a power3 out curve. Hiding is a 0.5 s power2
// in-out slide. An optional SVG turbulence filter roughens the shape, its noise frequency breathing on a 4 s sine loop.
// Autoplay hides and drops it on a loop (it holds while the pointer or keyboard focus is on it); with autoplay off it
// follows scrolling, on the nearest scrolling ancestor or the window: scrolling down past 100 px hides it, scrolling up
// drops it. The Test button hides it fast (0.3 s power2 in) and drops it. Reduced motion fades it out and in instead,
// with no slide and no wiggle. Animation pauses off screen and in hidden tabs.
// mount(root, { autoplay ("on" loop or "off" follow scroll), wiggle (px the edge is pulled down), speed (drop duration, s),
//               distortion (turbulence strength), pause (s between drops), links (comma separated, "Label" or "Label=url"),
//               cta (button text), colour1 (header left), colour2 (header right), ink (header text), background, text })
function mount(root, o) {
  var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  var box = root.querySelector(".ef-wdh");
  var bar = root.querySelector(".ef-wdh__bar");
  var path = root.querySelector(".ef-wdh__path");
  var grad = root.querySelector(".ef-wdh__grad");
  var stop1 = root.querySelector(".ef-wdh__s1"), stop2 = root.querySelector(".ef-wdh__s2");
  var filter = root.querySelector(".ef-wdh__filter"), turb = root.querySelector(".ef-wdh__turb"), disp = root.querySelector(".ef-wdh__disp");
  var g = root.querySelector(".ef-wdh__g");
  var nav = root.querySelector(".ef-wdh__links"), cta = root.querySelector(".ef-wdh__cta");
  var lede = root.querySelector(".ef-wdh__p"), test = root.querySelector(".ef-wdh__test");
  // unique ids, so two headers on one page do not share a gradient or filter
  var uid = "ef-wdh-" + Math.random().toString(36).slice(2, 9);
  grad.id = uid + "-g"; filter.id = uid + "-f"; path.setAttribute("fill", "url(#" + grad.id + ")");

  var opts = {}, raf = 0, last = 0, clock = 0, visible = true, destroyed = false, io = null, inTick = false;
  var hy = 0, hidden = false, tweens = {}, queue = [], hover = false, focusIn = false, mode = "", linkSig = "", flat = true;
  var lastTop = 0, lastSrc = null;

  // GSAP's eases, written out
  function power2In(p) { return p * p * p; }
  function power2InOut(p) { return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; }
  function power3Out(p) { return 1 - Math.pow(1 - p, 4); }
  var EA = 1.2, EP = 0.3, E3 = EP / (2 * Math.PI) * Math.asin(1 / EA), E2 = 2 * Math.PI / EP;
  function elasticOut(p) { return p >= 1 ? 1 : EA * Math.pow(2, -10 * p) * Math.sin((p - E3) * E2) + 1; } // elastic.out(1.2, 0.3)

  function num(v, d) { v = Number(v); return isFinite(v) ? v : d; }
  function shape(w) { return "M0,0 L2278,0 L2278,80 C2278,80, 1814," + w + ", 1139," + w + " C464," + w + ", 0,80, 0,80 Z"; }
  function setY(v) { hy = v; bar.style.transform = v ? "translate3d(0," + v.toFixed(3) + "%,0)" : ""; }
  function setW(v) { flat = Math.abs(v - 80) < 0.01; path.setAttribute("d", shape(flat ? 80 : v.toFixed(2))); }

  // a tiny tween engine: one running tween per key, rendered from the shared clock
  function tween(key, from, to, dur, ease, apply, done) {
    tweens[key] = { from: from, to: to, dur: Math.max(0.001, dur), ease: ease, apply: apply, done: done, t: 0 };
    apply(from);
    kick();
  }
  function after(sec, fn) { queue.push({ at: clock + sec, fn: fn }); kick(); }

  function hide(dur, ease, done) {
    hidden = true;
    if (reduce) { box.classList.add("is-off"); if (done) after(dur, done); return; }
    tween("y", hy, -100, dur, ease, setY, done);
  }
  function drop() {
    hidden = false;
    if (reduce) { box.classList.remove("is-off"); return; }
    tween("y", hy, 0, opts.speed * 0.44, power3Out, setY);
    tween("d", opts.wiggle, 80, opts.speed, elasticOut, setW);
  }
  function loopDrop() { drop(); after(opts.speed + opts.pause, loopHide); }
  function loopHide() {
    if (hover || focusIn) { after(0.3, loopHide); return; }
    hide(0.5, power2InOut);
    after(0.85, loopDrop);
  }
  function startMode() {
    queue = [];
    if (opts.autoplay === "on" && reduce) {
      lede.textContent = "Reduced motion is on, so the header stays put. Press Test animation to fade it out and back.";
      if (hidden) drop();
    } else if (opts.autoplay === "on") {
      lede.textContent = "This preview loops on its own. On a page, scrolling down hides the header and scrolling up drops it back in with a wiggle.";
      if (hidden) after(0.35, loopDrop); else after(opts.pause, loopHide);
    } else {
      lede.textContent = "Scroll down to hide the header, scroll up to drop it back in with a wiggle. Or press Test animation.";
      if (hidden) drop();
    }
  }
  function onTest() {
    queue = [];
    hide(0.3, power2In, function () {
      drop();
      if (opts.autoplay === "on" && !reduce) after(opts.speed + opts.pause, loopHide);
    });
  }

  // scroll mode: listen in the capture phase, so the window and any scrolling ancestor of the header are both heard
  function onScroll(e) {
    if (opts.autoplay === "on" || destroyed) return;
    var t = e.target, src = (!t || t === document || t === document.documentElement || t === document.body) ? null : t;
    if (src && !(src.contains && src.contains(box))) return;
    var st = src ? src.scrollTop : (window.pageYOffset || document.documentElement.scrollTop || 0);
    if (src !== lastSrc) { lastSrc = src; lastTop = 0; }
    if (st > lastTop && st > 100) { if (!hidden) { queue = []; hide(0.5, power2InOut); } }
    else if (st < lastTop) { if (hidden) drop(); }
    lastTop = st <= 0 ? 0 : st;
  }

  function busy() {
    for (var k in tweens) return true;
    return queue.length > 0 || (opts.distortion > 0 && !reduce);
  }
  function tick(now) {
    raf = 0; inTick = true;
    var dt = Math.min(0.05, (now - last) / 1000 || 0); last = now;
    if (visible && !document.hidden) {
      clock += dt;
      for (var k in tweens) {
        var tw = tweens[k]; tw.t += dt;
        var p = Math.min(1, tw.t / tw.dur);
        tw.apply(tw.from + (tw.to - tw.from) * tw.ease(p));
        if (p >= 1) { delete tweens[k]; if (tw.done) tw.done(); }
      }
      var due = queue.filter(function (q) { return q.at <= clock; });
      if (due.length) { queue = queue.filter(function (q) { return q.at > clock; }); due.forEach(function (q) { q.fn(); }); }
      if (opts.distortion > 0 && !reduce) {
        var tt = clock % 8, ph = tt < 4 ? tt / 4 : (8 - tt) / 4; // 0.004 to 0.009 and back, 4 s each way, sine in-out
        turb.setAttribute("baseFrequency", (0.004 + 0.005 * (1 - Math.cos(Math.PI * ph)) / 2).toFixed(5));
      }
    }
    inTick = false;
    if (busy() && !destroyed) raf = requestAnimationFrame(tick);
  }
  function kick() { if (!raf && !inTick && !destroyed) { last = performance.now(); raf = requestAnimationFrame(tick); } }

  function buildLinks() {
    var sig = String(opts.links);
    if (sig === linkSig) return;
    linkSig = sig; nav.textContent = "";
    sig.split(",").map(function (s) { var k = s.indexOf("="); return k < 0 ? { t: s.trim(), href: "#" } : { t: s.slice(0, k).trim(), href: s.slice(k + 1).trim() || "#" }; })
      .filter(function (l) { return l.t; }).slice(0, 6).forEach(function (l) {
        var a = document.createElement("a"); a.className = "ef-wdh__link"; a.href = l.href; a.textContent = l.t;
        nav.appendChild(a);
      });
  }
  function onDemoClick(e) { var a = e.target.closest && e.target.closest("a"); if (a && a.getAttribute("href") === "#") e.preventDefault(); }
  function onEnter() { hover = true; }
  function onLeave() { hover = false; }
  function onFocusIn() { focusIn = true; if (hidden) { queue = []; drop(); if (opts.autoplay === "on" && !reduce) after(opts.speed + opts.pause, loopHide); } }
  function onFocusOut() { focusIn = bar.contains(document.activeElement); }
  function onVis() { if (!document.hidden) kick(); }

  function update(partial) {
    opts = Object.assign({
      autoplay: "on", wiggle: 240, speed: 1.8, distortion: 0, pause: 1.5, links: "Library, Tools, Pricing", cta: "Get started",
      colour1: "#fbf8f1", colour2: "#e6ff55", ink: "#0c1e18", background: "#0c1e18", text: "#fbf8f1"
    }, opts, partial || {});
    opts.wiggle = Math.max(0, num(opts.wiggle, 240)); opts.speed = Math.max(0.1, num(opts.speed, 1.8));
    opts.distortion = Math.max(0, num(opts.distortion, 0)); opts.pause = Math.max(0, num(opts.pause, 1.5));
    opts.autoplay = opts.autoplay === "off" || opts.autoplay === false ? "off" : "on";
    box.style.setProperty("--ef-wdh-bg", opts.background);
    box.style.setProperty("--ef-wdh-text", opts.text);
    box.style.setProperty("--ef-wdh-ink", opts.ink);
    stop1.setAttribute("stop-color", opts.colour1);
    stop2.setAttribute("stop-color", opts.colour2);
    disp.setAttribute("scale", String(opts.distortion));
    if (opts.distortion > 0) g.setAttribute("filter", "url(#" + filter.id + ")"); else g.removeAttribute("filter");
    cta.textContent = opts.cta;
    buildLinks();
    if (destroyed) return;
    if (opts.autoplay !== mode) { mode = opts.autoplay; startMode(); }
    if (busy()) kick();
  }

  box.classList.toggle("is-still", reduce);
  // autoplay starts hidden and drops in straight away; reduced motion starts with the header in place
  if (!reduce && (o && o.autoplay === "off") !== true) { setY(-100); hidden = true; }
  update(o);
  test.addEventListener("click", onTest);
  bar.addEventListener("click", onDemoClick);
  bar.addEventListener("pointerenter", onEnter);
  bar.addEventListener("pointerleave", onLeave);
  bar.addEventListener("focusin", onFocusIn);
  bar.addEventListener("focusout", onFocusOut);
  document.addEventListener("scroll", onScroll, { passive: true, capture: true });
  document.addEventListener("visibilitychange", onVis);
  if ("IntersectionObserver" in window) { io = new IntersectionObserver(function (e) { visible = e[0].isIntersecting; if (visible) kick(); }); io.observe(box); }
  return {
    update: update,
    destroy: function () {
      destroyed = true; cancelAnimationFrame(raf); raf = 0; tweens = {}; queue = [];
      test.removeEventListener("click", onTest);
      bar.removeEventListener("click", onDemoClick);
      bar.removeEventListener("pointerenter", onEnter);
      bar.removeEventListener("pointerleave", onLeave);
      bar.removeEventListener("focusin", onFocusIn);
      bar.removeEventListener("focusout", onFocusOut);
      document.removeEventListener("scroll", onScroll, { passive: true, capture: true });
      document.removeEventListener("visibilitychange", onVis);
      if (io) io.disconnect();
    }
  };
}

// Usage
var fx = mount(document.querySelector("#my-root"), {"autoplay":"on","wiggle":240,"speed":1.8,"distortion":0,"pause":1.5,"links":"Library, Tools, Pricing","cta":"Get started","colour1":"#fbf8f1","colour2":"#e6ff55","ink":"#0c1e18","background":"#0c1e18","text":"#fbf8f1"});

How it works

The header's colour is one SVG path, stretched across the width, whose bottom edge is two curves meeting in the middle. To drop the header in, the three middle points of that edge are set to the wiggle amount at once, so the edge sags into a deep bulge, and then eased back to a straight line on an elastic out curve (amplitude 1.2, period 0.3) over the drop duration. At the same time the header slides down from minus 100 percent in 44 percent of that time on a power3 out curve, so it lands first and the edge keeps wobbling. Hiding is a 0.5 second power2 in-out slide up. Autoplay hides and drops it on a loop for previews that cannot scroll. With autoplay off it listens to scrolling on the window and on any scrolling ancestor: scrolling down past 100 pixels hides it and any scroll up drops it back. The optional distortion runs the shape through an SVG turbulence and displacement filter whose noise frequency drifts between 0.004 and 0.009 on a 4 second sine loop. All tweens are a few lines of easing maths on one requestAnimationFrame clock, in place of GSAP.

Accessibility and performance

The logo, links and button are real links and a button inside a header and a labelled nav, with visible focus rings. If keyboard focus lands in the header while it is hidden, it drops back in, and the autoplay loop waits while the pointer or focus is on it. The Test animation button replays the drop. With prefers-reduced-motion there is no slide and no wiggle: autoplay is off and the header simply fades out and back.

Each frame changes at most one transform and one path attribute, and only while a tween runs; with autoplay off the frame loop stops completely when nothing is moving. The scroll listener is passive. The turbulence filter is only attached when distortion is above zero, because SVG filters repaint the whole shape every frame. Animation pauses off screen and in hidden tabs.

SectionNavigation and lists
TechniquesScroll-driven, Reveal and intro
StackVanilla JS, SVG
Works withReact, Next.js, Vanilla
DependenciesNone
Statustested
On CodePenOpen the live pen on our CodePen account; fork it to experiment.
Inspired bySmart Wiggle Header by Enrichr on CodePen. Original code, written from scratch.

Find this technique on a real site

Want to see this technique in the wild? Check any site with the free tools, then build your own version from this page.

Check which animation library a site uses · Extract a site's design tokens · Browse site breakdowns

Licence and provenance

Free to use in personal and client projects during the preview. Final licence terms will be published here before any paid tier starts; ask before reselling the code as a collection.

This is original code. The technique is a common one that we studied by measuring award-winning sites with Enrichr; no third-party code, shaders, fonts or images are used.

Mask shape sectionsThe same image revealed through a circle, a diamond and a band as each section scrolls into view. Image compare on scrollA before and after slider whose divider is scrubbed by scroll position and can also be dragged or keyboard-operated. Unroll image on scrollA poster that unrolls like a sheet of paper from a spiral roll as you scroll or hover.

FAQ

How do I make it a real fixed header?

Set autoplay to Off and give .ef-wdh__bar position: fixed (top: 0, left: 0, width: 100%). It then listens to window scrolling: down hides it, up drops it back with the wiggle.

Why does the bottom edge wobble?

The edge is pulled down by the wiggle amount and released on an elastic curve, which overshoots past straight, swings back and settles, like a spring. Raise the drop duration for a slower, looser wobble.

Is this the same as the Wiggle magnet button?

No. That is a button that leans toward the pointer. This is a full-width header that hides and drops in with a wobbly edge.

Does it need GSAP?

No. The original used GSAP to tween the header and the path. Here the same eases, power3 out, power2 in-out and elastic out (1.2, 0.3), are written out and driven by one requestAnimationFrame loop.