Components / Motion / Dock

Dock beta

The macOS dock, in vanilla: tiles swell as the cursor approaches and settle as it leaves.

Preview

Sweep the cursor across — slowly. The wave is the part.

<div style="background:var(--k-zinc-950);border-radius:14px;min-height:170px;display:flex;align-items:center;justify-content:center;padding:18px">
  <div class="k-dock" data-dock><button class="k-dock-i" type="button" data-label="Home" aria-label="Home" style="background:var(--k-ink)"><svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square" stroke-linejoin="bevel" aria-hidden="true"><path d="m3 11 9-8 9 8"/><path d="M5 9.5V20a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V9.5"/></svg></button><button class="k-dock-i" type="button" data-label="Calendar" aria-label="Calendar" style="background:var(--k-accent)"><svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square" stroke-linejoin="bevel" aria-hidden="true"><rect x="3.5" y="5" width="17" height="16" rx="2"/><path d="M3.5 10h17M8 3v4M16 3v4"/></svg></button><button class="k-dock-i" type="button" data-label="Reports" aria-label="Reports" style="background:var(--k-zinc-500)"><svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square" stroke-linejoin="bevel" aria-hidden="true"><path d="M6 20v-6M12 20V4M18 20v-9"/><path d="M3.5 20.5h17"/></svg></button><button class="k-dock-i" type="button" data-label="Docs" aria-label="Docs" style="background:var(--k-danger)"><svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square" stroke-linejoin="bevel" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5Z"/><path d="M14 3v5h5M9 13h6M9 17h6"/></svg></button><button class="k-dock-i" type="button" data-label="Alerts" aria-label="Alerts" style="background:var(--k-zinc-700)"><svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square" stroke-linejoin="bevel" aria-hidden="true"><path d="M18 16H6c.6-1.2 1-2.6 1-4.5V10a5 5 0 0 1 10 0v1.5c0 1.9.4 3.3 1 4.5Z"/><path d="M10 20a2 2 0 0 0 4 0"/></svg></button><button class="k-dock-i" type="button" data-label="Team" aria-label="Team" style="background:var(--k-success)"><svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square" stroke-linejoin="bevel" aria-hidden="true"><circle cx="12" cy="8" r="3.6"/><path d="M5 20c.8-3.2 3.6-5 7-5s6.2 1.8 7 5"/></svg></button><button class="k-dock-i" type="button" data-label="Settings" aria-label="Settings" style="background:var(--k-zinc-800)"><svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square" stroke-linejoin="bevel" aria-hidden="true"><path d="M3 6h10M17 6h4M3 12h4M11 12h10M3 18h12M19 18h2"/><circle cx="15" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="17" cy="18" r="2"/></svg></button></div>
</div>
<p style="font-size:12.5px;color:var(--k-ink-3);margin-top:9px">Sweep the cursor across — slowly. The wave is the part.</p>
/* KATA Dock — tiles swell by proximity. */
.k-dock{display:inline-flex;align-items:flex-end;gap:7px;padding:9px 10px;background:var(--k-bg-soft);border:1px solid var(--k-line);border-radius:20px;font-family:var(--k-font)}
.k-dock-i{position:relative;width:46px;height:46px;border:0;border-radius:13px;color:var(--k-on-color);display:flex;align-items:center;justify-content:center;cursor:pointer;transform-origin:bottom center;transition:transform .14s ease-out;flex:none}
.k-dock-i::after{content:attr(data-label);position:absolute;bottom:calc(100% + 9px);left:50%;transform:translateX(-50%) translateY(3px);background:var(--k-ink);color:var(--k-bg);font-size:11px;font-weight:550;padding:4px 9px;border-radius:6px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .15s,transform .15s}
.k-dock-i:hover::after,.k-dock-i:focus-visible::after{opacity:1;transform:translateX(-50%) translateY(0)}
// KATA Dock — scale by distance from the cursor; rest when it leaves.
document.querySelectorAll('[data-dock]').forEach(function (dock) {
  var tiles = Array.prototype.slice.call(dock.querySelectorAll('.k-dock-i'));
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  dock.addEventListener('pointermove', function (e) {
    tiles.forEach(function (t) {
      var r = t.getBoundingClientRect();
      var d = Math.abs(e.clientX - (r.left + r.width / 2));
      var s = 1 + 0.55 * Math.max(0, 1 - d / 120);
      t.style.transform = 'scale(' + s.toFixed(3) + ')';
    });
  });
  dock.addEventListener('pointerleave', function () {
    tiles.forEach(function (t) { t.style.transform = ''; });
  });
});

Installation

CLI

node kata-ui.mjs add dock

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 app launchers and playful navigation. The script measures each tile’s distance from the cursor and scales by proximity — near tiles grow, the wave falls off within about two tiles. Tiles are buttons or links with real names; the label above a hovered tile is its name, not a decoration.

Accessibility

Magnification is pointer theatre: focus order, names and layout never move, and a keyboard user tabs through ordinary buttons. Under prefers-reduced-motion the wave is off and every tile sits at rest size.

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-dockclass
.k-dock-iclass
data-dockdata hook
data-labeldata hook
aria-labelARIA
aria-hiddenARIA

Related parts