← Attention

Float

The element bobs gently up and down, as if hovering, while a soft shadow beneath it breathes: smaller and fainter as it rises, fuller as it sinks back.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-float to any element. It floats while it’s on screen.

Options

Add any of these attributes to the same element.

data-heightHow far it rises, in px. Default 12.
data-durationHow long one rise and fall takes. Default 3000 ms.
data-tiltRock this many degrees as it bobs. Default 0.
data-gapSpace between its bottom edge and the shadow, in px. Default 14.
data-shadowSet to false for no shadow.
data-delayWait before it starts. Default 0 ms.

The shadow is added inside the element and sits just below it, so don’t clip the element’s overflow, and leave a little room underneath. Elements that can’t hold children, like <img>, float without a shadow: wrap them to get one.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Float: the element bobs gently up and down, as if hovering, while a soft
    // shadow beneath it breathes: smaller and fainter as it rises, fuller as it
    // sinks back. It pauses while off screen.
    // Usage: <div data-float>…</div>
    // Optional: data-height (px it rises, default 12), data-duration (ms for one
    // rise and fall, default 3000), data-tilt (degrees it rocks as it bobs,
    // default 0), data-gap (px from its bottom edge to the shadow, default 14),
    // data-shadow="false" (no shadow), data-delay (ms before it starts).
    // The shadow sits just below the element, so don't clip its overflow.
    // An <img> can't hold the shadow: wrap it in a div to get one.
    var DEFAULTS = { height: 12, duration: 3000, tilt: 0, gap: 14, delay: 0 };

    var css =
      '.flt-shadow{position:absolute;left:50%;top:100%;width:76%;height:12px;margin-left:-38%;border-radius:50%;' +
      'background:radial-gradient(closest-side,rgba(15,15,30,.32),rgba(15,15,30,0));pointer-events:none}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    function setup(el) {
      var d = el.dataset;
      var s = (el._flt = {});
      // Elements that can't hold children (img, input…) float without a shadow.
      if (d.shadow !== 'false' && !/^(IMG|INPUT|VIDEO|CANVAS|TEXTAREA|SELECT)$/.test(el.tagName)) {
        if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
        s.shadow = document.createElement('span');
        s.shadow.className = 'flt-shadow';
        s.shadow.setAttribute('aria-hidden', 'true');
        s.shadow.style.marginTop = (d.gap === undefined ? DEFAULTS.gap : +d.gap) + 'px';
        el.appendChild(s.shadow);
      }
    }

    function start(el) {
      var d = el.dataset;
      var s = el._flt;
      var h = d.height === undefined ? DEFAULTS.height : +d.height;
      var tilt = +d.tilt || 0;
      var p = { t: 0 };
      s.anim = anime.animate(p, {
        t: 1,
        duration: +d.duration || DEFAULTS.duration,
        ease: 'linear',
        loop: true,
        autoplay: false,
        onUpdate: function () {
          var lift = 0.5 - 0.5 * Math.cos(p.t * Math.PI * 2); // 0 at rest, 1 at the top
          var y = -h * lift;
          var r = tilt * Math.sin(p.t * Math.PI * 2);
          el.style.transform = 'translateY(' + y.toFixed(2) + 'px) rotate(' + r.toFixed(2) + 'deg)';
          if (s.shadow) {
            // The shadow stays on the ground: undo the lift and the tilt.
            s.shadow.style.transform = 'rotate(' + (-r).toFixed(2) + 'deg) translateY(' + (-y).toFixed(2) + 'px) scale(' + (1 - 0.3 * lift).toFixed(3) + ')';
            s.shadow.style.opacity = (1 - 0.5 * lift).toFixed(3);
          }
        }
      });
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          var el = e.target;
          var s = el._flt;
          s.visible = e.isIntersecting;
          if (e.isIntersecting && !s.started) {
            s.started = true;
            setTimeout(function () {
              start(el);
              if (s.visible) s.anim.play();
            }, +el.dataset.delay || DEFAULTS.delay);
          } else if (s.anim) {
            if (s.visible) s.anim.play();
            else s.anim.pause();
          }
        });
      });
      document.querySelectorAll('[data-float]').forEach(function (el) {
        if (el._flt) return;
        setup(el);
        if (!reduced()) io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div class="hero-art" data-float><img src="rocket.png" alt=""></div>

<!-- or higher, with a slight rock: -->
<span class="hero-art" data-float data-height="20" data-tilt="2">…</span>