Components / KATAYABURI / Gravity Playground
Gravity Playground beta
The catalog, dropped: chips of parts fall, bounce, pile and can be thrown across the stage.
Grab a chip and throw it. The pile is not load-bearing.
<div class="k-grav" data-grav role="img" aria-label="Chips named after catalog parts, falling and piling under gravity"> <span class="k-grav-chip">Button</span><span class="k-grav-chip">Modal</span><span class="k-grav-chip">Toast</span><span class="k-grav-chip hot">QR Code</span><span class="k-grav-chip">Sidebar</span><span class="k-grav-chip hot">Dock</span><span class="k-grav-chip">Calendar</span><span class="k-grav-chip">Meteor</span><span class="k-grav-chip">Kanban</span><span class="k-grav-chip">Aurora</span> </div> <p style="font-size:12.5px;color:var(--k-ink-3);margin-top:9px">Grab a chip and throw it. The pile is not load-bearing.</p>
/* KATA Gravity Playground — KATAYABURI. The catalog, dropped. */
.k-grav{position:relative;width:100%;background:var(--k-zinc-950);border-radius:14px;overflow:hidden;height:264px;font-family:var(--k-font);touch-action:none}
.k-grav-chip{position:absolute;left:0;top:0;padding:8px 14px;border-radius:99px;background:var(--k-zinc-800);color:var(--k-on-color);font-size:13px;font-weight:600;white-space:nowrap;cursor:grab;border:1px solid var(--k-zinc-700);user-select:none;-webkit-user-select:none}
.k-grav-chip.hot{background:var(--k-accent);border-color:var(--k-accent)}
.k-grav-chip:active{cursor:grabbing}
// KATA Gravity Playground — circles in chip clothing; grab and fling.
document.querySelectorAll('[data-grav]').forEach(function (stage) {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var bodies = [];
stage.querySelectorAll('.k-grav-chip').forEach(function (el, i) {
var r = Math.max(el.offsetWidth, el.offsetHeight) / 2;
bodies.push({ el: el, x: 30 + (i * 67) % Math.max(120, stage.clientWidth - 90), y: 16 + (i % 4) * 34, vx: 0, vy: 0, r: r, held: false });
});
var drag = null, last = null;
bodies.forEach(function (b) {
b.el.style.left = '0px'; b.el.style.top = '0px';
b.el.addEventListener('pointerdown', function (e) {
drag = b; last = { x: e.clientX, y: e.clientY, t: performance.now() };
b.held = true; b.vx = 0; b.vy = 0;
b.el.setPointerCapture(e.pointerId);
});
b.el.addEventListener('pointermove', function (e) {
if (drag !== b) return;
var now = performance.now();
var rect = stage.getBoundingClientRect();
b.x = e.clientX - rect.left; b.y = e.clientY - rect.top;
var dt = Math.max(1, now - last.t);
b.vx = (e.clientX - last.x) / dt * 16; b.vy = (e.clientY - last.y) / dt * 16;
last = { x: e.clientX, y: e.clientY, t: now };
});
function release() { if (drag === b) { drag = null; b.held = false; } }
b.el.addEventListener('pointerup', release);
b.el.addEventListener('pointercancel', release);
});
function tick() {
var W = stage.clientWidth, H = stage.clientHeight;
bodies.forEach(function (b) {
if (!b.held) {
b.vy += .45;
b.x += b.vx; b.y += b.vy;
b.vx *= .999; b.vy *= .999;
if (b.x < b.r) { b.x = b.r; b.vx = Math.abs(b.vx) * .5; }
if (b.x > W - b.r) { b.x = W - b.r; b.vx = -Math.abs(b.vx) * .5; }
if (b.y < b.el.offsetHeight / 2) { b.y = b.el.offsetHeight / 2; b.vy = Math.abs(b.vy) * .5; }
if (b.y > H - b.el.offsetHeight / 2) { b.y = H - b.el.offsetHeight / 2; b.vy = -Math.abs(b.vy) * .42; b.vx *= .92; if (Math.abs(b.vy) < .6) b.vy = 0; }
}
b.el.style.transform = 'translate(' + (b.x - b.el.offsetWidth / 2) + 'px,' + (b.y - b.el.offsetHeight / 2) + 'px)';
});
for (var i = 0; i < bodies.length; i++) for (var j = i + 1; j < bodies.length; j++) {
var a = bodies[i], c = bodies[j];
var dx = c.x - a.x, dy = c.y - a.y;
var rr = (a.el.offsetWidth + c.el.offsetWidth) / 4.4;
var d2 = dx * dx + dy * dy;
if (d2 > 0 && d2 < rr * rr) {
var d = Math.sqrt(d2), push = (rr - d) / 2;
var nx = dx / d, ny = dy / d;
if (!a.held) { a.x -= nx * push; a.y -= ny * push; }
if (!c.held) { c.x += nx * push; c.y += ny * push; }
}
}
requestAnimationFrame(tick);
}
if (reduced) {
bodies.forEach(function (b, i) { b.x = 40 + i * 62; b.y = stage.clientHeight - 24; b.el.style.transform = 'translate(' + (b.x - b.el.offsetWidth / 2) + 'px,' + (b.y - b.el.offsetHeight / 2) + 'px)'; });
} else { requestAnimationFrame(tick); }
});
Installation
CLI
node kata-ui.mjs add gravity-playground
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
Every chip is a part name with a small physics body: gravity, wall bounces, cheap collisions, and a pointer that can grab and fling. It is a toy — the kind a conference booth runs on a big screen. Put it anywhere the brand can afford to be caught playing.
Accessibility
A toy, honestly labeled: the chips are inert text to assistive technology (the same names exist in the Components index, in order). Under prefers-reduced-motion the chips rest in a neat pile at the floor — gravity, but polite.
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-grav | class |
.k-grav-chip | class |
.hot | class |
data-grav | data hook |
aria-label | 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.