Tabs

General

Name your project and choose its default design style.

@use 'sprucecss/scss/spruce' as *;

.tabs {
  @include layout-stack('m');

  &__list {
    border-block-end: config('border-width-base', $display) solid color('border');
    display: flex;
    gap: spacer('xs');
    overflow-x: auto;
    scrollbar-width: thin;
  }

  &__tab {
    @include clear-btn;
    border-block-end: 2px solid transparent;
    color: color('muted');
    font-weight: 600;
    margin-block-end: calc(#{config('border-width-base', $display)} * -1);
    padding: spacer('xs') spacer('s');
    white-space: nowrap;
    @include transition($property: 'border-color, color');

    &:hover {
      color: color('heading');
    }

    &[aria-selected='true'] {
      border-block-end-color: color('primary');
      color: color('heading');
    }

    &:focus-visible {
      border-radius: radius('sm');
      outline: 2px solid color('focus-ring');
      outline-offset: -2px;
    }
  }

  &__panel {
    @include layout-stack('xs');

    h3 {
      font-size: font-size('h5');
    }

    &:focus-visible {
      border-radius: radius('sm');
      outline: 2px solid color('focus-ring');
      outline-offset: 4px;
    }
  }
}
<div class="tabs" data-tabs>
    <div class="tabs__list" role="tablist" aria-label="Project settings">
        <button class="tabs__tab" type="button" role="tab" id="tab-general" aria-controls="panel-general" aria-selected="true">General</button>
        <button class="tabs__tab" type="button" role="tab" id="tab-members" aria-controls="panel-members" aria-selected="false" tabindex="-1">Members</button>
        <button class="tabs__tab" type="button" role="tab" id="tab-billing" aria-controls="panel-billing" aria-selected="false" tabindex="-1">Billing</button>
    </div>
    <div class="tabs__panel" role="tabpanel" id="panel-general" aria-labelledby="tab-general" tabindex="0">
        <h3>General</h3>
        <p>Name your project and choose its default design style.</p>
    </div>
    <div class="tabs__panel" role="tabpanel" id="panel-members" aria-labelledby="tab-members" tabindex="0" hidden>
        <h3>Members</h3>
        <p>Invite teammates and manage their roles.</p>
    </div>
    <div class="tabs__panel" role="tabpanel" id="panel-billing" aria-labelledby="tab-billing" tabindex="0" hidden>
        <h3>Billing</h3>
        <p>Update your plan, payment method and invoices.</p>
    </div>
</div>
(() => {
  document.querySelectorAll('[data-tabs]').forEach((component) => {
    const tabs = [...component.querySelectorAll('[role="tab"]')];

    const select = (tab, setFocus = true) => {
      tabs.forEach((item) => {
        const isSelected = item === tab;

        item.setAttribute('aria-selected', String(isSelected));
        item.tabIndex = isSelected ? 0 : -1;
        document.getElementById(item.getAttribute('aria-controls')).hidden = !isSelected;
      });

      if (setFocus) {
        tab.focus();
      }
    };

    tabs.forEach((tab, index) => {
      tab.addEventListener('click', () => select(tab, false));

      tab.addEventListener('keydown', (event) => {
        const isRtl = getComputedStyle(component).direction === 'rtl';
        const next = isRtl ? 'ArrowLeft' : 'ArrowRight';
        const previous = isRtl ? 'ArrowRight' : 'ArrowLeft';
        let target = null;

        if (event.key === next) target = tabs[(index + 1) % tabs.length];
        if (event.key === previous) target = tabs[(index - 1 + tabs.length) % tabs.length];
        if (event.key === 'Home') target = tabs[0];
        if (event.key === 'End') target = tabs[tabs.length - 1];

        if (target) {
          event.preventDefault();
          select(target);
        }
      });
    });
  });
})();

Tabs

On this page

Accessible tabs following the WAI-ARIA Tabs pattern, with automatic activation and full keyboard support.

Technical Details

  • Roles: tablist, tab (buttons) and tabpanel; each tab controls its panel (aria-controls) and each panel is labelled by its tab.
  • Roving tabindex: only the selected tab is in the tab order. ←/→ move between tabs (mirrored in RTL), Home/End jump to the first/last.
  • Panels are focusable (tabindex="0") so keyboard users can reach content that has no interactive element.
  • Hidden panels use the hidden attribute, so without JavaScript you can render all of them visible.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.