Tiny·Engine (Core)

Recipe

Tabs

An ARIA-compliant tab strip with arrow-key navigation in one capsule.

Markup

tabs.html
<div ui-tabs role="tablist">
  <button ref="tab0" data-index="0" role="tab" aria-controls="p1">One</button>
  <button ref="tab1" data-index="1" role="tab" aria-controls="p2">Two</button>
  <button ref="tab2" data-index="2" role="tab" aria-controls="p3">Three</button>

  <section ref="panel0" id="p1" role="tabpanel">...</section>
  <section ref="panel1" id="p2" role="tabpanel" hidden>...</section>
  <section ref="panel2" id="p3" role="tabpanel" hidden>...</section>
</div>

Capsule

tabs.ts
import { UI, Capsule } from "tiny-engine-core";

class Tabs extends Capsule {
  constructor(el, options) {
    super(el, options);
    this.tabs = Array.from(this.el.querySelectorAll("[role=tab]"));
    this.panels = Array.from(this.el.querySelectorAll("[role=tabpanel]"));

    this.activate(0, false);
    this.on(this.el, "click", (event) => {
      const tab = event.target.closest("[role=tab]");
      if (tab) this.activate(Number(tab.dataset.index));
    });
    this.on(this.el, "keydown", (event) => {
      const current = this.tabs.indexOf(document.activeElement);
      if (event.key === "ArrowRight") this.activate((current + 1) % this.tabs.length);
      if (event.key === "ArrowLeft") this.activate((current - 1 + this.tabs.length) % this.tabs.length);
    });
  }

  activate(index, focus = true) {
    this.tabs.forEach((tab, i) => {
      tab.setAttribute("aria-selected", String(i === index));
      tab.tabIndex = i === index ? 0 : -1;
    });
    this.panels.forEach((panel, i) => panel.toggleAttribute("hidden", i !== index));
    if (focus) this.tabs[index].focus();
  }
}

UI.register("tabs", Tabs);
UI.init();