<div class="tabs-to-accordion" data-tabs-container>
    <ul class="tabs-to-accordion__list" role="tablist" aria-label="">
        <li class="tabs-to-accordion__list-item" data-tabs-button-wrap>
            <button class="tabs-to-accordion__button" role="tab" aria-selected="false" data-tabs-button>
          Test Heading
        </button>
        </li>
        <li class="tabs-to-accordion__list-item" data-tabs-button-wrap>
            <button class="tabs-to-accordion__button" role="tab" aria-selected="false" data-tabs-button>
          Test Heading II
        </button>
        </li>
        <li class="tabs-to-accordion__list-item" data-tabs-button-wrap>
            <button class="tabs-to-accordion__button" role="tab" aria-selected="false" data-tabs-button>
          Test Heading III
        </button>
        </li>
    </ul>
    <div class="tabs-to-accordion__window" data-tabs-panel-window>
        <div class="tabs-to-accordion__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-to-accordion__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-to-accordion__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-to-accordion" data-tabs-container>
  <ul class="tabs-to-accordion__list" role="tablist" aria-label="{{ heading }}">
    {% for tab in tabs %}
      <li class="tabs-to-accordion__list-item" data-tabs-button-wrap>
        <button class="tabs-to-accordion__button" role="tab" aria-selected="false" data-tabs-button>
          {{ tab.heading }}
        </button>
      </li>
    {% endfor %}
  </ul>
  <div class="tabs-to-accordion__window" data-tabs-panel-window>
    {% for tab in tabs %}
      <div class="tabs-to-accordion__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:
    .tabs-to-accordion {
    
      &__list {
        margin: 0;
        padding: 0;
        font-size: 0;
      }
    
      &__list-item {
        display: block;
        margin: 0;
        list-style: none;
        font-size: 0;
    
        @media (min-width: $breakpoint-sm) {
          display: inline-block;
        }
      }
    
      &__list-item + &__list-item {
    
        @media (min-width: $breakpoint-sm) {
          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;
          }
        }
      }
    
      &__button[aria-selected='true'] + &__panel {
        display: block;
      }
    
      &__panel {
        display: none;
    
        &[data-tabs-panel-active='true'] {
          display: block;
        }
      }
    }
    
  • URL: /components/raw/tabs-to-accordion/_tabs-to-accordion.scss
  • Filesystem Path: components/tabs-to-accordion/_tabs-to-accordion.scss
  • Size: 1.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;
    }
    
    (function() {
      // Tabs Functionality
      const components = document.querySelectorAll('[data-tabs-container]');
      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];
          let tabSiblings = getSiblings(tabWrap);
          let panelSiblings = getSiblings(panel);
    
          tabs[i].setAttribute('aria-selected', 'true');
          tabs[i].setAttribute('tabindex', '0');
          panels[i].setAttribute('data-tabs-panel-active', 'true');
    
          tabSiblings.forEach((sibling) => {
            sibling.querySelector('[data-tabs-button]').setAttribute('aria-selected', 'false');
            sibling.querySelector('[data-tabs-button]').setAttribute('tabindex', '-1');
          });
    
          // Need to adjust this declaration to be specfic bbl
          if (components[0].offsetWidth <= 385) {
            panels.forEach((panel) => {
              panel.setAttribute('data-tabs-panel-active', 'false');
            })
          }
        });
    
        // 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;
      };
    
      function mobileCheck() {
    
        for (let i = 0; i < components.length; i++) {
          const panelsWindow = components[i].querySelectorAll('[data-tabs-panel-window]')[0];
    
          if (components[i].offsetWidth <= 385) {
            for (let i = 0; i < tabs.length; i++) {
              const associatedPanel = panels[i];
              tabs[i].after(associatedPanel);
    
              panelsWindow.innerHTML = '';
            }
          } else {
            if (!panelsWindow.hasChildNodes()) {
              panels.forEach((panel) => {
                panelsWindow.prepend(panel);
              });
            }
          }
        }
      }
    
      if (tabs.length > 0) {
        tabs[0].click();
      }
    
      window.addEventListener('resize', () => {
        mobileCheck();
      });
    })()
  • URL: /components/raw/tabs-to-accordion/tabs-to-accordion.js
  • Filesystem Path: components/tabs-to-accordion/tabs-to-accordion.js
  • Size: 3.4 KB

Usage

This component displays content in a tabbed format on desktop, and transitions to display the content in an accordion on 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".

Focus Expectations

  • Tab and accordion headers should have visible keyboard focus state
  • All keyboard interactions relate to when headers are focused

Keyboard Expectations

  • Space or Enter = Activate tabs and accordion panels
  • = Cycles tab and accordion panel focus
  • Control + Option = Navigate through tab and accordion content with screen reader