Components / KATAYABURI / Particle Word
Particle Word beta
A word made of a thousand dots that scatter when you touch them — the KATAYABURI flagship.
Run the cursor through the word. It forgives you, slowly.
<div class="k-pw" data-pw data-word="KATA" role="img" aria-label="The word KATA, made of particles">
<canvas aria-hidden="true"></canvas>
<div class="k-pw-bar">
<button class="k-pw-btn on" type="button" data-pw-word="KATA">KATA</button>
<button class="k-pw-btn" type="button" data-pw-word="型破り">型破り</button>
<button class="k-pw-btn" type="button" data-pw-word="2026">2026</button>
</div>
</div>
<p style="font-size:12.5px;color:var(--k-ink-3);margin-top:9px">Run the cursor through the word. It forgives you, slowly.</p>/* KATA Particle Word — KATAYABURI. A word, weather included. */
.k-pw{background:var(--k-zinc-950);border-radius:14px;padding:14px;font-family:var(--k-font)}
.k-pw canvas{display:block;width:100%;height:230px}
.k-pw-bar{display:flex;gap:8px;justify-content:center;margin-top:10px}
.k-pw-btn{border:1px solid var(--k-zinc-700);background:none;color:var(--k-zinc-300);font-family:inherit;font-size:12.5px;font-weight:550;padding:6px 13px;border-radius:99px;cursor:pointer}
.k-pw-btn:hover{color:var(--k-on-color);border-color:var(--k-zinc-500)}
.k-pw-btn.on{background:var(--k-on-color);color:var(--k-zinc-950);border-color:var(--k-on-color)}
// KATA Particle Word — samples a word into points; each point gets
// a spring home and a healthy fear of the cursor.
document.querySelectorAll('[data-pw]').forEach(function (stage) {
var canvas = stage.querySelector('canvas');
var ctx = canvas.getContext('2d');
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var W = 0, H = 0, parts = [], mouse = { x: -9999, y: -9999 };
var accent = getComputedStyle(stage).getPropertyValue('--k-accent').trim() || '#0e9594';
var paper = getComputedStyle(stage).getPropertyValue('--k-on-color').trim() || '#ffffff';
function targets(word) {
var off = document.createElement('canvas');
off.width = W; off.height = H;
var o = off.getContext('2d');
o.fillStyle = '#fff';
o.font = '700 ' + Math.min(150, W / Math.max(2.2, word.length * 0.62)) + 'px Inter, sans-serif';
o.textAlign = 'center'; o.textBaseline = 'middle';
o.fillText(word, W / 2, H / 2 + 4);
var data = o.getImageData(0, 0, W, H).data;
var pts = [], step = 5;
for (var y = 0; y < H; y += step) for (var x = 0; x < W; x += step)
if (data[(y * W + x) * 4 + 3] > 128) pts.push({ x: x, y: y });
if (pts.length > 1500) { pts = pts.filter(function (_, i) { return i % 2 === 0; }); }
return pts;
}
function seed(word) {
var pts = targets(word);
while (parts.length < pts.length) parts.push({ x: Math.random() * W, y: Math.random() * H, vx: 0, vy: 0, c: Math.random() < .22 ? accent : paper });
parts.length = pts.length;
parts.forEach(function (p, i) { p.hx = pts[i].x; p.hy = pts[i].y; if (reduced) { p.x = p.hx; p.y = p.hy; } });
}
function frame() {
ctx.clearRect(0, 0, W, H);
parts.forEach(function (p) {
var dx = mouse.x - p.x, dy = mouse.y - p.y;
var d2 = dx * dx + dy * dy;
if (d2 < 4900) { var d = Math.sqrt(d2) || 1; var f = (70 - d) / 70 * 2.6; p.vx -= dx / d * f; p.vy -= dy / d * f; }
p.vx += (p.hx - p.x) * 0.028;
p.vy += (p.hy - p.y) * 0.028;
p.vx *= 0.86; p.vy *= 0.86;
p.x += p.vx; p.y += p.vy;
ctx.fillStyle = p.c;
ctx.fillRect(p.x, p.y, 2.2, 2.2);
});
if (!reduced) requestAnimationFrame(frame);
}
function resize() {
W = canvas.clientWidth; H = canvas.clientHeight;
canvas.width = W; canvas.height = H;
seed(stage.getAttribute('data-word') || 'KATA');
if (reduced) frame();
}
stage.querySelectorAll('[data-pw-word]').forEach(function (b) {
b.addEventListener('click', function () {
stage.querySelectorAll('[data-pw-word]').forEach(function (x) { x.classList.remove('on'); });
b.classList.add('on');
stage.setAttribute('data-word', b.getAttribute('data-pw-word'));
seed(b.getAttribute('data-pw-word'));
});
});
canvas.addEventListener('pointermove', function (e) {
var r = canvas.getBoundingClientRect();
mouse.x = e.clientX - r.left; mouse.y = e.clientY - r.top;
});
canvas.addEventListener('pointerleave', function () { mouse.x = -9999; mouse.y = -9999; });
resize();
window.addEventListener('resize', resize);
if (!reduced) requestAnimationFrame(frame);
});
Installation
CLI
node kata-ui.mjs add particle-word
Writes the part’s CSS and JS into your project. New to the CLI? See the CLI doc.
Manual
- Load
tokens.cssonce, before any part. - Copy the CSS tab into your stylesheet.
- Copy the HTML where it belongs. The part has a script — include it after the markup; it initializes itself.
Usage
KATAYABURI is the shelf where the mold breaks on purpose. This one samples a word into points and gives every point a spring home and a fear of cursors. Change the word with the buttons, or the data-word attribute. It proves nothing, sells nothing, and is the part people will remember — which, occasionally, is the job.
Accessibility
The word also exists as plain text for assistive technology; the canvas is aria-hidden theatre. Under prefers-reduced-motion the dots assemble instantly and stay put — the word, without the weather.
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-pw | class |
.k-pw-bar | class |
.k-pw-btn | class |
.on | class |
data-pw | data hook |
data-word | data hook |
data-pw-word | data hook |
aria-label | ARIA |
aria-hidden | ARIA |
Related parts
Bounce Letters beta
A headline whose letters hop when touched — and do the wave when you click.
Carousel Ring beta
A merry-go-round of cards in real 3D: eight panels riding a ring that never stops turning.
Confetti Cannon beta
A button that fires confetti out of both corners of its stage. You already pressed it twice.
Fireworks beta
Click the night and it blooms: rockets, bursts and falling sparks on one canvas.