Components / Motion / Goo Tabs
Goo Tabs beta
Tabs whose indicator travels like liquid: it stretches mid-flight, then settles over the chosen tab.
<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
- 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
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-tabs | class |
.k-goo-bar | class |
.k-goo-ind | class |
.moving | class |
.k-goo-panel | class |
data-gootabs | data hook |
aria-label | ARIA |
aria-hidden | ARIA |
aria-controls | ARIA |
aria-selected | ARIA |
aria-labelledby | ARIA |
Related parts
Accordion Gallery beta
Five scenes in a row; the one you touch opens up and tells its caption.
Aurora beta
A slow wash of color behind a hero — the accent and the core, drifting under glass.
Beam border beta
A single point of light travels the edge of a card. The beam is the core color — the one accent, in motion.
Blur In beta
Words arrive out of focus and sharpen one after another, like eyes adjusting.