Components / Motion / Goo Tabs

Goo Tabs beta

Tabs whose indicator travels like liquid: it stretches mid-flight, then settles over the chosen tab.

Preview
The workspace at a glance: open orders, this week’s revenue, and the three parts everyone keeps copying.
<div class="k-goo-tabs" data-gootabs>
  <div class="k-goo-bar" role="tablist" aria-label="Workspace">
    <span class="k-goo-ind" aria-hidden="true"></span>
    <button role="tab" id="gt-0" aria-controls="gtp-0" aria-selected="true" tabindex="0">Overview</button><button role="tab" id="gt-1" aria-controls="gtp-1" aria-selected="false" tabindex="-1">Orders</button><button role="tab" id="gt-2" aria-controls="gtp-2" aria-selected="false" tabindex="-1">Customers</button>
  </div>
  <div class="k-goo-panel" role="tabpanel" id="gtp-0" aria-labelledby="gt-0">The workspace at a glance: open orders, this week’s revenue, and the three parts everyone keeps copying.</div><div class="k-goo-panel" role="tabpanel" id="gtp-1" aria-labelledby="gt-1" hidden>128 orders, newest first. Four are overdue — the toolbar above the table already knows which.</div><div class="k-goo-panel" role="tabpanel" id="gtp-2" aria-labelledby="gt-2" hidden>312 customers, 41 of them new this month. The list exports exactly what it shows.</div>
</div>
/* KATA Goo Tabs — the indicator travels like liquid. */
.k-goo-tabs{font-family:var(--k-font);color:var(--k-ink);max-width:520px}
.k-goo-bar{position:relative;display:inline-flex;background:var(--k-bg-soft);border:1px solid var(--k-line);border-radius:99px;padding:4px}
.k-goo-ind{position:absolute;top:4px;bottom:4px;border-radius:99px;background:var(--k-ink);transition:left .45s cubic-bezier(.6,-0.1,.2,1.25), width .45s cubic-bezier(.6,-0.1,.2,1.25), transform .3s}
.k-goo-ind.moving{transform:scaleY(.86)}
.k-goo-bar button{position:relative;z-index:1;border:0;background:none;font-family:inherit;font-size:13.5px;font-weight:600;color:var(--k-ink-2);padding:8px 16px;border-radius:99px;cursor:pointer;transition:color .25s}
.k-goo-bar button[aria-selected="true"]{color:var(--k-bg)}
.k-goo-panel{padding:16px 3px 2px;font-size:13.5px;line-height:1.75;color:var(--k-ink-2)}
@media (prefers-reduced-motion:reduce){.k-goo-ind{transition:none}}
// KATA Goo Tabs — real tabs; only the indicator is theatrical.
document.querySelectorAll('[data-gootabs]').forEach(function (root) {
  var bar = root.querySelector('.k-goo-bar');
  var ind = root.querySelector('.k-goo-ind');
  var tabs = Array.prototype.slice.call(bar.querySelectorAll('button[role="tab"]'));
  var panels = Array.prototype.slice.call(root.querySelectorAll('[role="tabpanel"]'));
  function place(tab, animate) {
    ind.style.left = tab.offsetLeft + 'px';
    ind.style.width = tab.offsetWidth + 'px';
    if (animate) {
      ind.classList.add('moving');
      ind.style.width = (tab.offsetWidth * 1.18) + 'px';
      setTimeout(function () { ind.style.width = tab.offsetWidth + 'px'; ind.classList.remove('moving'); }, 180);
    }
  }
  function choose(tab, focus) {
    tabs.forEach(function (t) {
      var on = t === tab;
      t.setAttribute('aria-selected', on);
      t.tabIndex = on ? 0 : -1;
      var p = panels[tabs.indexOf(t)];
      if (p) p.hidden = !on;
    });
    place(tab, true);
    if (focus) tab.focus();
  }
  tabs.forEach(function (t) { t.addEventListener('click', function () { choose(t, false); }); });
  bar.addEventListener('keydown', function (e) {
    var i = tabs.indexOf(document.activeElement);
    if (i < 0) return;
    if (e.key === 'ArrowRight') { choose(tabs[(i + 1) % tabs.length], true); e.preventDefault(); }
    if (e.key === 'ArrowLeft') { choose(tabs[(i - 1 + tabs.length) % tabs.length], true); e.preventDefault(); }
  });
  var start = tabs.filter(function (t) { return t.getAttribute('aria-selected') === 'true'; })[0] || tabs[0];
  choose(start, false);
  place(start, false);
});

Installation

CLI

node kata-ui.mjs add goo-tabs

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

A working tab set — buttons, panels, arrow keys — whose indicator refuses to move like a spreadsheet. It slides with an elastic curve and stretches a fifth wider mid-journey, which is all the goo a production interface can defend. The panels are real panels; switch the content, keep the plumbing.

Accessibility

Full tab semantics: role=tablist, aria-selected, one tab stop, arrow-key travel — the liquid never touches the keyboard model. Under prefers-reduced-motion the indicator jumps without stretching, still landing exactly where it belongs.

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-goo-tabsclass
.k-goo-barclass
.k-goo-indclass
.movingclass
.k-goo-panelclass
data-gootabsdata hook
aria-labelARIA
aria-hiddenARIA
aria-controlsARIA
aria-selectedARIA
aria-labelledbyARIA

Related parts