Components / KATAYABURI / Confetti Cannon

Confetti Cannon beta

A button that fires confetti out of both corners of its stage. You already pressed it twice.

Preview
<div class="k-conf" data-conf>
  <canvas aria-hidden="true"></canvas>
  <button class="k-conf-btn" type="button" data-conf-fire>Payment complete — fire</button>
</div>
/* KATA Confetti Cannon — KATAYABURI. On-token confetti. */
.k-conf{position:relative;width:100%;background:var(--k-zinc-950);border-radius:14px;overflow:hidden;font-family:var(--k-font);min-height:210px;display:flex;align-items:center;justify-content:center}
.k-conf canvas{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
.k-conf-btn{position:relative;z-index:2;background:var(--k-on-color);color:var(--k-zinc-950);border:0;font-family:inherit;font-size:14.5px;font-weight:650;padding:12px 24px;border-radius:99px;cursor:pointer}
.k-conf-btn:active{transform:scale(.96)}
// KATA Confetti Cannon — canvas pieces, token colors, real gravity.
document.querySelectorAll('[data-conf]').forEach(function (stage) {
  var canvas = stage.querySelector('canvas');
  var ctx = canvas.getContext('2d');
  var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  var cs = getComputedStyle(stage);
  var COLORS = ['--k-accent', '--k-on-color', '--k-zinc-400', '--k-amber-ink'].map(function (v) { return cs.getPropertyValue(v).trim(); }).filter(Boolean);
  var pieces = [], raf = null;
  function size() { canvas.width = stage.clientWidth; canvas.height = stage.clientHeight; }
  function burst() {
    if (reduced) return;
    size();
    [0, canvas.width].forEach(function (ox, side) {
      var dir = side === 0 ? 1 : -1;
      for (var i = 0; i < 70; i++) {
        var a = (-58 - Math.random() * 38) * Math.PI / 180;
        var sp = 9 + Math.random() * 9;
        pieces.push({ x: ox, y: canvas.height + 4, vx: Math.cos(a) * sp * dir, vy: Math.sin(a) * sp, w: 5 + Math.random() * 5, h: 3 + Math.random() * 5, rot: Math.random() * Math.PI, vr: (Math.random() - .5) * .3, c: COLORS[i % COLORS.length], life: 1 });
      }
    });
    if (!raf) tick();
  }
  function tick() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    pieces = pieces.filter(function (p) { return p.life > 0 && p.y < canvas.height + 30; });
    pieces.forEach(function (p) {
      p.vy += .22; p.vx *= .99; p.vy *= .995;
      p.x += p.vx; p.y += p.vy; p.rot += p.vr;
      if (p.y > canvas.height - 90) p.life -= .02;
      ctx.save();
      ctx.globalAlpha = Math.max(0, Math.min(1, p.life));
      ctx.translate(p.x, p.y); ctx.rotate(p.rot);
      ctx.fillStyle = p.c;
      ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h * (0.4 + Math.abs(Math.sin(p.rot * 2)) * .6));
      ctx.restore();
    });
    raf = pieces.length ? requestAnimationFrame(tick) : null;
    if (!raf) ctx.clearRect(0, 0, canvas.width, canvas.height);
  }
  stage.querySelector('[data-conf-fire]').addEventListener('click', burst);
  size();
  if ('IntersectionObserver' in window && !reduced) {
    var io = new IntersectionObserver(function (es) { if (es[0].isIntersecting) { setTimeout(burst, 350); io.disconnect(); } }, { threshold: .5 });
    io.observe(stage);
  }
});

Installation

CLI

node kata-ui.mjs add confetti-cannon

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

For the moments software should celebrate out loud: the deploy, the signup, the invoice marked paid. Two cannons fire from the stage corners in the token palette — even the confetti is on-token. It fires once when it scrolls into view, then waits for the button like a loaded party.

Accessibility

The celebration is visual only: the success it marks must be announced in words by the thing it celebrates ("Payment complete" — the Result part exists for exactly this). Under prefers-reduced-motion the cannon stays unloaded and the button simply says its line.

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-confclass
.k-conf-btnclass
data-confdata hook
data-conf-firedata hook
aria-hiddenARIA

Related parts