Tab

Как сверстать табы.

Статья о доступности Aria: tab role

Статья на Доке

Пример использования

HTML

<section class="js-tabs base-tabs">
  <div role="tablist" aria-label="Sample Tabs">
    <button
      role="tab"
      aria-selected="true"
      aria-controls="panel-1"
      id="tab-1"
      tabindex="0"
      class="base-tabs__button"
    >
      First Tab
    </button>
    <button
      role="tab"
      aria-selected="false"
      aria-controls="panel-2"
      id="tab-2"
      tabindex="-1"
      class="base-tabs__button"
    >
      Second Tab
    </button>
    <button
      role="tab"
      aria-selected="false"
      aria-controls="panel-3"
      id="tab-3"
      tabindex="-1"
      class="base-tabs__button"
    >
      Third Tab
    </button>
  </div>
  <div id="panel-1" role="tabpanel" tabindex="0" aria-labelledby="tab-1">
    <p>Content for the first panel</p>
  </div>
  <div id="panel-2" role="tabpanel" tabindex="0" aria-labelledby="tab-2" hidden>
    <p>Content for the second panel</p>
  </div>
  <div id="panel-3" role="tabpanel" tabindex="0" aria-labelledby="tab-3" hidden>
    <p>Content for the third panel</p>
  </div>
</section>

TS

const TABS_SELECTOR = ".js-tabs";
const TABLIST_SELECTOR = '[role="tablist"]';
const TABPANEL_SELECTOR = '[role="tabpanel"]';
const TAB_SELECTOR = '[role="tab"]';
const tabsElements = document.querySelectorAll<HTMLElement>(TABS_SELECTOR);

function initTabs(element: HTMLElement) {
  const tabList = element.querySelector<HTMLElement>(TABLIST_SELECTOR);
  if (!tabList) return;
  let tabFocus = 0;
  const tabPanelElements =
    element.querySelectorAll<HTMLElement>(TABPANEL_SELECTOR);
  const tabs = tabList.querySelectorAll<HTMLButtonElement>(TAB_SELECTOR);

  function handleTabClick(e: Event) {
    const targetTab = e.target as HTMLButtonElement;
    const activeButtonElements =
      tabList?.querySelectorAll(':scope > [aria-selected="true"]') ?? [];

    // Remove all current selected tabs
    for (const button of activeButtonElements) {
      button.setAttribute("aria-selected", "false");
    }
    // Set this tab as selected
    targetTab.setAttribute("aria-selected", "true");

    // Hide all tab panels
    for (const tabPanelElement of tabPanelElements) {
      tabPanelElement.setAttribute("hidden", "true");
    }

    // Show the selected panel
    element
      .querySelector(`#${targetTab.getAttribute("aria-controls")}`)
      ?.removeAttribute("hidden");
  }

  function changeTab(e: KeyboardEvent) {
    // Move right
    if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
      tabs[tabFocus].setAttribute("tabindex", "-1");
      if (e.key === "ArrowRight") {
        tabFocus++;
        // If we're at the end, go to the start
        if (tabFocus >= tabs.length) {
          tabFocus = 0;
        }
        // Move left
      } else {
        tabFocus--;
        // If we're at the start, move to the end
        if (tabFocus < 0) {
          tabFocus = tabs.length - 1;
        }
      }

      tabs[tabFocus].setAttribute("tabindex", "0");
      tabs[tabFocus].focus();
    }
  }

  for (const tab of tabs) {
    tab.addEventListener("click", handleTabClick);
  }

  tabList.addEventListener("keydown", changeTab);
}

for (const tabElement of tabsElements) {
  initTabs(tabElement);
}

Стили

Стилизовать активную кнопку через атрибут [aria-selected="true"]

.base-tabs {
  &__button {
    background-color: #fff;

    &[aria-selected="true"] {
      background-color: blue;
    }
  }
}