<div class="tabbed">
    <ul class="flat-list">
        <li>
            <a href="#section-0">
                <span>Tab 1</span>
            </a>
        </li>
        <li>
            <a href="#section-1">
                <svg class="icon " height="20" width="20" aria-hidden="true">
                    <use xlink:href="../../sprite.svg#inventory" />
                </svg>
                <span>Tab 2</span>
            </a>
        </li>
        <li>
            <a href="#section-2">
                <span>Tab 3</span>
            </a>
        </li>
        <li>
            <a href="#section-3">
                <span>Tab 4</span>
            </a>
        </li>
    </ul>
    <section id="section-">
        Panel content 1

    </section>
    <section id="section-">
        Panel content 2

    </section>
    <section id="section-">
        Panel content 3

    </section>
    <section id="section-">
        Panel content 4

    </section>
</div>
<div class="tabbed">
  <ul class="flat-list">
    {{#each tabs}}
      <li>
        <a href="#section-{{@index}}">
          {{#if icon}}
            {{> @svg}}
          {{/if}}
          <span>{{title}}</span>
        </a>
      </li>
    {{/each}}
  </ul>
  {{#> @partial-block}}
    {{#each tabs}}
      {{#> @tab-panel}}
        {{content}}
      {{/ @tab-panel}}
    {{/each}}
  {{/ @partial-block}}
</div>
{
  "tabs": [
    {
      "title": "Tab 1",
      "content": "Panel content 1\n"
    },
    {
      "title": "Tab 2",
      "icon": "inventory",
      "content": "Panel content 2\n"
    },
    {
      "title": "Tab 3",
      "content": "Panel content 3\n"
    },
    {
      "title": "Tab 4",
      "content": "Panel content 4\n"
    }
  ]
}
  • Content:
    /**
     * Tabbed interface
     */
    
    @media screen {
    
      [role="tablist"] {
        padding: 0.1em 0 0;
        display: flex;
      }
    
      @media (max-width: 24em) {
    
        [role="tablist"] {
          overflow-x: auto;
        }
      }
    
      [role="tablist"] li + li {
        margin-left: 0.25em;
      }
    
      [role="tablist"] a {
        display: inline-flex;
        align-items: center;
        background-color: var(--white);
        color: var(--body-text);
        text-decoration: none;
        padding: 0.5rem 1em;
        box-shadow: var(--tab-shadow);
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }
    
      [role="tablist"] a > * {
        margin-top: 0;
      }
    
      [role="tablist"] a .icon {
        height: 1.2em;
        width: 1.2em;
        margin-right: 0.25em;
      }
    
      [role="tablist"] [aria-selected] {
        background: var(--tab-accent);
        color: var(--white);
      }
    
      [role="tabpanel"] {
        border-top: 2px solid var(--tab-accent);
        border-bottom: 2px solid var(--tab-accent);
        padding: 1rem 0;
        margin-top: 0;
      }
    
      [role="tabpanel"]:focus {
        outline: 0;
      }
    }
    
  • URL: /components/raw/tab-interface/tab-interface.css
  • Filesystem Path: components/02_molecules/tab-interface/tab-interface.css
  • Size: 1 KB
  • Content:
    /**
     * Tabbed interface
     */
    
    'use strict';
    
    (function () {
    
      // Get relevant elements and collections
      const tabbed = document.querySelector('.tabbed');
    
      // Stop if we can’t find a tabbed element
      if (!tabbed) return;
    
      const tablist = tabbed.querySelector('ul');
      const tabs = tablist.querySelectorAll('a');
      const panels = tabbed.querySelectorAll('[id^="section"]');
    
      const findById = id => document.getElementById(id);
    
      // The tab switching function
      const switchTab = (oldTab, newTab) => {
        newTab.focus();
    
        // Make the active tab focusable by the user (Tab key)
        newTab.removeAttribute('tabindex');
    
        // Set the selected state
        newTab.setAttribute('aria-selected', 'true');
        oldTab.removeAttribute('aria-selected');
        oldTab.setAttribute('tabindex', '-1');
    
        // Get the indices of the new and old tabs to find the correct
        // tab panels to show and hide
        let index = Array.prototype.indexOf.call(tabs, newTab);
        let oldIndex = Array.prototype.indexOf.call(tabs, oldTab);
        panels[oldIndex].hidden = true;
        panels[index].hidden = false;
    
        // Update browser history API & URL
        let currentTab = tabs[index].getAttribute('id');
        let currentState = history.state;
        history.pushState(currentState, currentTab, '#' + currentTab);
      };
    
      // Add the tablist role to the first <ul> in the .tabbed container
      tablist.setAttribute('role', 'tablist');
    
      // Add semantics are remove user focusability for each tab
      Array.prototype.forEach.call(tabs, (tab, i) => {
        tab.setAttribute('role', 'tab');
        tab.setAttribute('id', 'tab' + (i + 1));
        tab.setAttribute('tabindex', '-1');
        tab.parentNode.setAttribute('role', 'presentation');
    
        // Handle clicking of tabs for mouse users
        tab.addEventListener('click', e => {
          e.preventDefault();
          let currentTab = tablist.querySelector('[aria-selected]');
          if (e.currentTarget !== currentTab) {
            switchTab(currentTab, e.currentTarget);
          }
        });
    
        // Handle keydown events for keyboard users
        tab.addEventListener('keydown', e => {
    
          // Get the index of the current tab in the tabs node list
          let index = Array.prototype.indexOf.call(tabs, e.currentTarget);
    
          // Work out which key the user is pressing and
          // Calculate the new tab's index where appropriate
          let dir = e.which === 37 ? index - 1 : e.which === 39 ? index + 1 : e.which === 40 ? 'down' : null;
    
          if (dir !== null) {
            e.preventDefault();
            // If the down key is pressed, move focus to the open panel,
            // otherwise switch to the adjacent tab
            dir === 'down' ? panels[i].focus() : tabs[dir] ? switchTab(e.currentTarget, tabs[dir]) : void 0;
          }
        });
      });
    
      // Add tab panel semantics and hide them all
      Array.prototype.forEach.call(panels, (panel, i) => {
        panel.setAttribute('role', 'tabpanel');
        panel.setAttribute('tabindex', '-1');
        panel.setAttribute('aria-labelledby', tabs[i].id);
        panel.hidden = true;
      });
    
      // Initially activate the first tab and reveal the first tab panel
      tabs[0].removeAttribute('tabindex');
      tabs[0].setAttribute('aria-selected', 'true');
      panels[0].hidden = false;
    
      // Switch tab via URL fragment if there is one
      if (window.location.hash) {
        let hash = window.location.hash.substring(1);
        let hashTab = findById(hash);
        let currentTab = tablist.querySelector('[aria-selected]');
        switchTab(currentTab, hashTab);
      }
    
    })();
    
  • URL: /components/raw/tab-interface/tab-interface.js
  • Filesystem Path: components/02_molecules/tab-interface/tab-interface.js
  • Size: 3.5 KB

Tabbed interface

JavaScript-driven, accessible tabbed interface. Supports keyboard navigation. Tabs can show either pure text, with or without icons, as well as icons only (which should be used with caution, as with all icon-only usages).