Tabs

<div class="tabs" data-tabs-container>
    <ul class="tabs__list" role="tablist" aria-label="">
        <li class="tabs__list-item" data-tabs-button-wrap>
            <button class="tabs__button" role="tab" aria-selected="false" data-tabs-button>
                    Test Heading
                </button>
        </li>
        <li class="tabs__list-item" data-tabs-button-wrap>
            <button class="tabs__button" role="tab" aria-selected="false" data-tabs-button>
                    Test Heading II
                </button>
        </li>
        <li class="tabs__list-item" data-tabs-button-wrap>
            <button class="tabs__button" role="tab" aria-selected="false" data-tabs-button>
                    Test Heading III
                </button>
        </li>
    </ul>
    <div class="tabs__window">
        <div class="tabs__panel" role="tabpanel" data-tabs-panel data-tabs-panel-active="false">
            <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sollicitudin suscipit laoreet. Ut tristique nulla tempus commodo pharetra. Nam auctor, urna id consequat vulputate, ipsum ante pulvinar dolor, sed rhoncus neque enim eu ipsum.
                Mauris viverra interdum mattis. Integer nec molestie tellus. Phasellus nec semper nibh, eget sagittis neque.</p>
        </div>
        <div class="tabs__panel" role="tabpanel" data-tabs-panel data-tabs-panel-active="false">
            <p>Vestibulum sollicitudin suscipit laoreet. Ut tristique nulla tempus commodo pharetra. Nam auctor, urna id consequat vulputate, ipsum ante pulvinar dolor, sed rhoncus neque enim eu ipsum. Mauris viverra interdum mattis. Integer nec molestie
                tellus. Phasellus nec semper nibh, eget sagittis neque.</p>
        </div>
        <div class="tabs__panel" role="tabpanel" data-tabs-panel data-tabs-panel-active="false">
            <p>Phasellus nec semper nibh, eget sagittis neque. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sollicitudin suscipit laoreet. Ut tristique nulla tempus commodo pharetra. Nam auctor, urna id consequat vulputate, ipsum ante
                pulvinar dolor, sed rhoncus neque enim eu ipsum. Mauris viverra interdum mattis. Integer nec molestie tellus. Phasellus nec semper nibh, eget sagittis neque.</p>
        </div>
    </div>
</div>
<div class="tabs" data-tabs-container>
    <ul class="tabs__list" role="tablist" aria-label="{{ heading }}">
        {% for tab in tabs %}
            <li class="tabs__list-item" data-tabs-button-wrap>
                <button class="tabs__button" role="tab" aria-selected="false" data-tabs-button>
                    {{ tab.heading }}
                </button>
            </li>
        {% endfor %}
    </ul>
    <div class="tabs__window">
        {% for tab in tabs %}
            <div class="tabs__panel" role="tabpanel" data-tabs-panel data-tabs-panel-active="false">
                {{ tab.content }}
            </div>
        {% endfor %}
    </div>
</div>
{
  "tabs": [
    {
      "heading": "Test Heading",
      "content": "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sollicitudin suscipit laoreet. Ut tristique nulla tempus commodo pharetra. Nam auctor, urna id consequat vulputate, ipsum ante pulvinar dolor, sed rhoncus neque enim eu ipsum. Mauris viverra interdum mattis. Integer nec molestie tellus. Phasellus nec semper nibh, eget sagittis neque.</p>"
    },
    {
      "heading": "Test Heading II",
      "content": "<p>Vestibulum sollicitudin suscipit laoreet. Ut tristique nulla tempus commodo pharetra. Nam auctor, urna id consequat vulputate, ipsum ante pulvinar dolor, sed rhoncus neque enim eu ipsum. Mauris viverra interdum mattis. Integer nec molestie tellus. Phasellus nec semper nibh, eget sagittis neque.</p>"
    },
    {
      "heading": "Test Heading III",
      "content": "<p>Phasellus nec semper nibh, eget sagittis neque. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sollicitudin suscipit laoreet. Ut tristique nulla tempus commodo pharetra. Nam auctor, urna id consequat vulputate, ipsum ante pulvinar dolor, sed rhoncus neque enim eu ipsum. Mauris viverra interdum mattis. Integer nec molestie tellus. Phasellus nec semper nibh, eget sagittis neque.</p>"
    }
  ]
}
  • Content:
    /*
     * tab styles (tabs on desktop and mobile)
     *
     *****************************************************************************/
    
    .tabs {
    
      &__list {
        margin: 0;
        padding: 0;
        font-size: 0;
      }
    
      &__list-item {
        display: inline-block;
        margin: 0;
        list-style: none;
        font-size: 0;
    
        & + & {
          border-left: 1px solid color(neutral, lighter);
        }
      }
    
      &__button {
        display: block;
        border: 0;
        border-bottom: 1px solid color(neutral, lighter);
        padding: 10px 20px;
        width: 100%;
        background-color: color(neutral, lightest);
        cursor: pointer;
        appearance: none;
    
        @media (min-width: $breakpoint-sm) {
            border-top: 1px solid color(neutral, lighter);
        }
    
        &[aria-selected='true'] {
            background-color: color(default, light);
    
            @media (min-width: $breakpoint-sm) {
                border-bottom: 0;
            }
        }
      }
    
      &__panel {
        display: none;
    
        &[data-tabs-panel-active='true'] {
          display: block;
        }
      }
    }
    
  • URL: /components/raw/tabs/_tabs.scss
  • Filesystem Path: components/tabs/_tabs.scss
  • Size: 1 KB
  • Content:
    // Polyfills
    if (window.Element && !Element.prototype.closest) {
      Element.prototype.closest =
      function(s) {
        var matches = (this.document || this.ownerDocument).querySelectorAll(s),
          i,
          el = this;
        do {
          i = matches.length;
          while (--i >= 0 && matches.item(i) !== el) {};
        } while ((i < 0) && (el = el.parentElement));
    
        return el;
      };
    }
    
    if (!Array.prototype.forEach) {
    	Array.prototype.forEach = function (callback, thisArg) {
    		thisArg = thisArg || window;
    		for (var i = 0; i < this.length; i++) {
    			callback.call(thisArg, this[i], i, this);
    		}
    	};
    }
    
    if (window.NodeList && !NodeList.prototype.forEach) {
      NodeList.prototype.forEach = Array.prototype.forEach;
    }
    
    // Tabs Functionality
    const tabs = document.querySelectorAll('[data-tabs-button]');
    const panels = document.querySelectorAll('[data-tabs-panel]');
    
    for (let i = 0; i < tabs.length; i++) {
    
      tabs[i].addEventListener('click', (e) => {
        const tabWrap = tabs[i].closest('[data-tabs-button-wrap]');
        const panel = panels[i];
        const panelSiblings = getSiblings(panel);
        let tabSiblings = getSiblings(tabWrap);
    
        tabs[i].setAttribute('aria-selected', 'true');
        tabs[i].setAttribute('tabindex', '0');
        panels[i].setAttribute('data-tabs-panel-active', 'true');
        panels[i].setAttribute('tabindex', '0');
        
        tabSiblings.forEach((sibling) => {
          sibling.querySelector('[data-tabs-button]').setAttribute('aria-selected', 'false');
          sibling.querySelector('[data-tabs-button]').setAttribute('tabindex', '-1');
        });
    
        panelSiblings.forEach((sibling) => {
          sibling.setAttribute('data-tabs-panel-active', 'false');
          sibling.setAttribute('tabindex', '-1');
        });
      });
    
      // Arrow key controls
      tabs[i].addEventListener('keydown', (e) => {
    
        if (e.keyCode === 37) {
          if (i === 0) {
            tabs[tabs.length - 1].focus();
          } else {
            tabs[i - 1].focus();
          }
        }
    
        if (e.keyCode === 39) {
          if (i === tabs.length - 1) {
            tabs[0].focus();
          } else {
            tabs[i + 1].focus();
          }
        }
      });
    }
    
    // This should probably be extracted into a more
    // global set of helper functions for pure JS
    // engineers down the road. - Greg S.
    function getSiblings(element) {
      let siblings = [];
      let sibling = element.parentNode.firstChild;
    
      while (sibling) {
        if (sibling.nodeType === 1 && sibling !== element) {
    			siblings.push(sibling);
        }
        sibling = sibling.nextSibling
      }
    
      return siblings;
    };
    
    if (tabs.length > 0) {
      tabs[0].click();
    }
    
  • URL: /components/raw/tabs/tabs.js
  • Filesystem Path: components/tabs/tabs.js
  • Size: 2.5 KB

Usage

Displays tabbed content with specific titles and descriptions grouped together. Only one tab can be visible at a time, with the first tab visible by default. This component functions as tabs on both desktop and mobile.

Labelling Expectations

  • The element that wraps tabs has role="tablist".
  • Each tab has role="tab" and is contained within the element with role="tablist".
  • Each tab content panel has role="tabpanel".
  • Each tab has the property aria-controls referring to its tabpanel.
  • The active tab has aria-selected="true" and all other tab elements have aria-selected="false".
  • The active tabpanel has tab-index="0" and all other tab elements have tab-index="-1".
  • Each tabpanel has the property aria-labelledby referring to its associated tab.

Focus Expectations

  • Tabs should have visible :focus state.
  • Tabs should have visible aria-selected="true" state.

Keyboard Expectations

  • When a tab has focus, Space or Enter key activates the tab, which displays the associated panel.
  • When a tabpanel has been activated and is displaying panel content, Tab moves focus to the corresponding tabpanel element, which is next in the tab sequence.
  • Shift + Tab will move focus back to the activating tab.
  • = Cycles tab focus. If focus is on the last tab, moves focus to the first tab. If focus is on the first tab, moves focus to the last tab.
  • = Cycles tab focus (vertical tabs). If focus is on the last tab, moves focus to the first tab. If focus is on the first tab, moves focus to the last tab.
  • Home = Focus first tab
  • End = Focus last tab