Components / KATAYABURI / Goo Cursor

Goo Cursor beta

A slime that follows the cursor: blobs with surface tension, courtesy of an SVG goo filter.

Preview
<div class="k-goo" data-goo role="img" aria-label="A gooey slime that follows the cursor">
  <div class="k-goo-field"></div>
  <p>Move through the stage. It follows like it means it.</p>
</div>
/* KATA Goo Cursor — KATAYABURI. Surface tension as a service. */
.k-goo{position:relative;width:100%;background:var(--k-zinc-950);border-radius:14px;overflow:hidden;height:250px;font-family:var(--k-font)}
.k-goo-field{position:absolute;inset:0}
.k-goo-b{position:absolute;border-radius:50%;background:var(--k-accent);transform:translate(-50%,-50%);pointer-events:none}
.k-goo p{position:relative;z-index:2;text-align:center;padding-top:104px;font-size:13px;color:var(--k-zinc-400);pointer-events:none}
// KATA Goo Cursor — a chain of lazy blobs under a goo filter.
document.querySelectorAll('[data-goo]').forEach(function (stage) {
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  var svgNS = 'http://www.w3.org/2000/svg';
  var svg = document.createElementNS(svgNS, 'svg');
  svg.setAttribute('width', '0'); svg.setAttribute('height', '0');
  svg.style.position = 'absolute';
  var filter = document.createElementNS(svgNS, 'filter');
  filter.setAttribute('id', 'k-goo-filter');
  var blur = document.createElementNS(svgNS, 'feGaussianBlur');
  blur.setAttribute('in', 'SourceGraphic'); blur.setAttribute('stdDeviation', '9');
  var matrix = document.createElementNS(svgNS, 'feColorMatrix');
  matrix.setAttribute('values', '1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 22 -11');
  filter.appendChild(blur); filter.appendChild(matrix);
  svg.appendChild(filter);
  stage.appendChild(svg);
  var field = stage.querySelector('.k-goo-field');
  field.style.filter = 'url(#k-goo-filter)';
  var SIZES = [44, 34, 27, 21, 16, 12];
  var blobs = SIZES.map(function (s, i) {
    var el = document.createElement('span');
    el.className = 'k-goo-b';
    el.style.width = s + 'px'; el.style.height = s + 'px';
    field.appendChild(el);
    return { el: el, x: stage.clientWidth / 2, y: stage.clientHeight / 2, ease: .34 - i * .045 };
  });
  var mouse = { x: stage.clientWidth / 2, y: stage.clientHeight / 2 };
  stage.addEventListener('pointermove', function (e) {
    var r = stage.getBoundingClientRect();
    mouse.x = e.clientX - r.left; mouse.y = e.clientY - r.top;
  });
  (function frame() {
    var tx = mouse.x, ty = mouse.y;
    blobs.forEach(function (b) {
      b.x += (tx - b.x) * b.ease;
      b.y += (ty - b.y) * b.ease;
      b.el.style.left = b.x + 'px';
      b.el.style.top = b.y + 'px';
      tx = b.x; ty = b.y;
    });
    requestAnimationFrame(frame);
  })();
});

Installation

CLI

node kata-ui.mjs add goo-cursor

Writes the part’s CSS and JS into your project. New to the CLI? See the CLI doc.

Manual

  1. Load tokens.css once, before any part.
  2. Copy the CSS tab into your stylesheet.
  3. Copy the HTML where it belongs. The part has a script — include it after the markup; it initializes itself.

Usage

Six blobs chase the pointer in a chain, each lazier than the last, and a blur-and-contrast filter fuses them into one creature with surface tension. It serves no purpose a meeting could survive. It is also the first thing everyone touches — some parts are for the hands, not the roadmap.

Accessibility

Pointer theatre in its purest form: invisible to assistive technology, absent on touch, and fully still under prefers-reduced-motion. The stage beneath it must carry its own meaning, because the goo carries none.

Anatomy

The public surface of this part, read from its own source. Class names are the API — the CSS tab above is the full contract.

.k-gooclass
.k-goo-fieldclass
.k-goo-bclass
data-goodata hook
aria-labelARIA

Related parts