Home
Head's Up: I'm in the middle of upgrading my site. Most things are in place, but there are something missing and/or broken including image alt text. Please bear with me while I'm getting things fixed.

Accessible HTML Tabs For Web Pages

I'm adding tabs to show different examples for a color picker in Neopoligen neo .

I found a video on how to make "CSS only" tabs that looked good but never mentioned accessibility. I checked with folks on a few discords for guidance and learned it's got problems. Specifically, it's not currently possible to make CSS only tabs in an accessible way.

Chris Ferdinandi from Go Make Things gmt and Mayank from mayank.co mayank sent over some better alternatives. Here's Mayank's approach with some specific styles added into the CSS below.

This is my local copy of the code for my notes. I've added nothing new here except a few styles. Be sure to visit Mayank's post for the original code and details

Shut the hatch before the waves push it in

JavaScript

class TabGroup extends HTMLElement {
  get tabs() {
    return [...this.querySelectorAll('[role=tab]')];
  }

  get panels() {
    return [...this.querySelectorAll('[role=tabpanel]')];
  }

  get selected() {
    return this.querySelector('[role=tab][aria-selected=true]');
  }

  set selected(element) {
    this.selected?.setAttribute('aria-selected', 'false');
    element?.setAttribute('aria-selected', 'true');
    element?.focus();
    this.updateSelection();
  }

  connectedCallback() {
    this.generateIds();
    this.updateSelection();
    this.setupEvents();
  }

  generateIds() {
    const prefix = Math.floor(Date.now()).toString(36);
    this.tabs.forEach((tab, index) => {
      const panel = this.panels[index];
      tab.id ||= `${prefix}-tab-${index}`;
      panel.id ||= `${prefix}-panel-${index}`;
      tab.setAttribute('aria-controls', panel.id);
      panel.setAttribute('aria-labelledby', tab.id);
    });
  }

  updateSelection() {
    this.tabs.forEach((tab, index) => {
      const panel = this.panels[index];
      const isSelected = tab.getAttribute('aria-selected') === 'true';
      tab.setAttribute('aria-selected', isSelected ? 'true' : 'false');
      tab.setAttribute('tabindex', isSelected ? '0' : '-1');
      panel.setAttribute('tabindex', isSelected ? '0' : '-1');
      panel.hidden = !isSelected;
    });
  }

  setupEvents() {
    this.tabs.forEach((tab) => {
      tab.addEventListener('click', () => this.selected = tab);
      tab.addEventListener('keydown', (e) => {
        if (e.key === 'ArrowLeft') {
          this.selected = tab.previousElementSibling ?? this.tabs.at(-1);
        } else if (e.key === 'ArrowRight') {
          this.selected = tab.nextElementSibling ?? this.tabs.at(0);
        }
      });
    });
  }
}

customElements.define('tab-group', TabGroup);

CSS

:root {
  --color-selected: rgb(255 255 255);
  --color-not-selected: rgb(255 255 255 / .6);
}

[role="tab"] {
  background: none;
  border: none;
  color: var(--color-not-selected);
  cursor: pointer;
  font: inherit;
  outline: inherit;
  padding-block: 0 2px;
  padding-inline: 11px;
  &[aria-selected='true'] {
    border-bottom: 3px solid var(--color-selected);
    color: var(--color-selected);
    padding-block: 0 0;
  }
}

[role="tablist"] {
  border-bottom: 1px solid var(--color-selected);
}

[role="tabpanel"] {
  padding: 0.7rem;
}

I'm not linking to the original video since the technique isn't accessible and I don't want to add to its SEO. It's called "How to Easily Create Pure CSS Tabs (No JavaScript Needed!)" by "dcode" if you want to look it up

Footnotes And References