Responsive Tabs to Accordion

  • Traverse through the tabs using the arrow keys
  • Enter a panel by either tabbing into it or using PAGE DOWN
  • To get back up to the active tab while within a panel, use PAGE UP
  • Resize the browser to see the responsive change from tabs to accordion

Please fill in your preferred contact info

Home

HTML Source Code

<h2>Please fill in your preferred contact info</h2>
<div id="tab-container" aria-multiselectable="false" class="tabs-view">
  <ul id="navlist" role="tablist">
    <li role="presentation" class="active">
      <a role="tab" id="one" aria-setsize="3" aria-posinset="1" tabindex="0" href="#" aria-controls="panel-1" aria-selected="true">Home</a>
    </li>
    <li role="presentation">
      <a role="tab" id="two" aria-setsize="3" aria-posinset="2" tabindex="-1" href="#" aria-controls="panel-2" aria-selected="false">Work</a>
    </li>
    <li role="presentation">
      <a role="tab" id="three" aria-setsize="3" aria-posinset="3" tabindex="-1" href="#" aria-controls="panel-3" aria-selected="false">Other</a>
    </li>
  </ul>
  <div id="panels">
    <div role="tabpanel" aria-labelledby="one" class="panel current" tabindex="-1" id="panel-1">
      <h3>Home</h3>
      <div class="panel-body">
        <div class="field">
          <label for="home-email">Home Email</label>
          <input type="text" id="home-email">
        </div>
        <div class="field">
          <label for="home-phone">Home Phone</label>
          <input type="text" id="home-phone">
        </div>
        <button type="button">Submit</button>
      </div>
    </div>
    <div aria-hidden="true" class="panel" role="tabpanel" aria-labelledby="two" tabindex="-1" id="panel-2">
      <h3>Work</h3>
      <div class="panel-body">
        <div class="field">
          <label for="work-email">Work Email</label>
          <input type="text" id="work-email">
        </div>
        <div class="field">
          <label for="work-phone">Work Phone</label>
          <input type="text" id="work-phone">
        </div>
        <button type="button">Submit</button>
      </div>
    </div>
    <div aria-hidden="true" class="panel" role="tabpanel" aria-labelledby="three" tabindex="-1" id="panel-3">
      <h3>Other</h3>
      <div class="panel-body">
        <div class="field">
          <label for="other-email">Other Email</label>
          <input type="text" id="other-email">
        </div>
        <div class="field">
          <label for="other-phone">Other Phone</label>
          <input type="text" id="other-phone">
        </div>
        <button type="button">Submit</button>
      </div>
    </div>
  </div>
</div>

JavaScript Source Code

Dependencies:
  • JQuery
  • JQuery UI
var $navlist = $('#navlist');
var $tabContainer = $('#tab-container');
var $panels = $('#panels');

$navlist.on('keydown', 'li a', function (keyVent) {
  var arrows = [37, 38, 39, 40];
  var which = keyVent.which;
  var target = keyVent.target;
  if ($.inArray(which, arrows) > -1) {
    var adjacentTab = findAdjacentTab(target, $navlist, which);

    if (adjacentTab) {
      keyVent.preventDefault();
      adjacentTab.focus();
      // if desired behavior is that when tab recieves focus -> make it the active tab:
      setActiveAndInactive(adjacentTab, $navlist);
    }
  } else if (which === 13 || which === 32) { // ENTER |or| SPACE
    keyVent.preventDefault(); // don't scroll the page around...
    target.click();
  } else if (which === 34) { // PAGE DOWN
    keyVent.preventDefault(); // don't scroll the page
    var assocPanel = $('#' + this.getAttribute('aria-controls'));
    if (assocPanel) {
      assocPanel.focus();
    }
  }
});

$(document.body).on('keydown', '.panel', function (e) {
  if (e.which === 33) { // PAGE UP
    e.preventDefault(); // don't scroll
    var activeTab = $navlist.find('li.active a')[0];
    if (activeTab) {
      activeTab.focus();
    }
  }
});

// click support
$navlist.on('click', 'li a', function () {
  setActiveAndInactive(this, $navlist);
});

function findAdjacentTab(startTab, $list, key) {
  var dir = (key === 37 || key === 38) ? 'prev' : 'next';
  var adjacentTab = (dir === 'prev') ?
                    $(startTab.parentNode).prev()[0] :
                    $(startTab.parentNode).next()[0];

  if (!adjacentTab) {
    var allTabs = $list.find('li');
    if (dir === 'prev') {
      adjacentTab = allTabs[allTabs.length - 1];
    } else {
      adjacentTab = allTabs[0];
    }
  }

  return $(adjacentTab).find('a')[0];
}

function setActiveAndInactive(newActive, $list) {
  $list.find('li').each(function () {
    var assocPanelID = $(this)
                          .find('a')
                          .first()
                          .attr('aria-controls');
    var anchor = $(this).find('a')[0];

    if (this !== newActive.parentNode) {
      $(this).removeClass('active');
      anchor.tabIndex = -1;
      anchor.setAttribute('aria-selected', 'false');
      $('#' + assocPanelID)
        .removeClass('current')
        .attr('aria-hidden', 'true');
    } else {
      $(this).addClass('active');
      anchor.tabIndex = 0;
      anchor.setAttribute('aria-selected', 'true');
      $('#' + assocPanelID)
        .addClass('current')
        .removeAttr('aria-hidden');
    }

  });
}

// initial configuration based on window's width
var isAccordionView = false;
var isTabsView = false;

determineView();

// Debounced Resize() jQuery Plugin
// Author: Paul Irish
(function($, sr){
  // debouncing function from John Hann
  // http://unscriptable.com/index.php/2009/03/20/debouncing-javascript-methods/
  var debounce = function (func, threshold, execAsap) {
      var timeout;

      return function debounced () {
          var obj = this, args = arguments;
          function delayed () {
              if (!execAsap)
                  func.apply(obj, args);
              timeout = null;
          }

          if (timeout)
              clearTimeout(timeout);
          else if (execAsap)
              func.apply(obj, args);

          timeout = setTimeout(delayed, threshold || 100);
      };
  };
  // smartresize
  jQuery.fn[sr] = function(fn){  return fn ? this.bind('resize', debounce(fn)) : this.trigger(sr); };

})(jQuery,'smartresize');


// RESIZE EVENT:
$(window).smartresize(determineView);


function determineView() {
  var winWidth = $(window).width();

  if (winWidth <= 800 && !isAccordionView) { // SHOW ACCORDION VIEW
    // switch to the accordion view
    $tabContainer
      .removeClass('tabs-view')
      .addClass('accordion-view');

    // fix the markup to be more suited for accordions
    $panels.find('.panel').each(function () {
      var panelID = this.id;
      var assocLink = panelID && $('#navlist a[aria-controls="' + panelID + '"]')[0];
      if (assocLink) {
        $(assocLink.parentNode).append(this);
      }
    });

    isAccordionView = true;
    isTabsView = false;
  } else if (winWidth > 800 && !isTabsView) { // SHOW TABS VIEW
    var wasAccordion = $tabContainer.hasClass('accordion-view');
    // switch to the tabs view
    $tabContainer
      .removeClass('accordion-view')
      .addClass('tabs-view');

    if (wasAccordion) {
      $navlist.find('.panel').each(function () {
        $panels.append(this);
      });
    }

    isTabsView = true;
    isAccordionView = false;
  }
}

CSS Source Code

Dependencies:

  • JQuery UI theme: Smoothness (including some images)
.tabs-view #navlist {
  padding: 3px 0px 0px;
  margin: 0px;
  border-bottom: 1px solid #778;
}

.tabs-view #navlist li {
  list-style: none;
  margin: 0px;
  display: inline;
}

.tabs-view #navlist > li > a {
  font-size: 18px;
  padding: 3px 0.5em;
  border: 1px solid #778;
  border-bottom: none;
  background: #DDE;
  text-decoration: none;
}

.tabs-view #navlist .active a {
  background: white;
  border-bottom: 1px solid white;
}
.tabs-view #panels {
  border: 1px solid;
  border-top: none;
}
.tabs-view .panel {
  display: none;
  text-align: center;
  padding: 10px;
  font-size: 18px;
}
.tabs-view .panel.current {
  display: block !important;
}

#tab-container.tabs-view {
  width: 85%;
  margin: 0 auto;
}

/*ACCORDION VIEW*/
.accordion-view #navlist {
  padding: 3px 0px;
}

.accordion-view #navlist li {
  display: block;
  margin: 0px auto;
  padding:0px;
  position: relative;
}
.accordion-view #navlist > li > a {
  font-size: 24px;
  padding: 3px 0.5em;
  border: 1px solid #778;
  background: #DDE;
  text-decoration: none;
  display: inline-block;
  width: 100%;
  box-sizing: border-box;
}

.accordion-view #navlist .active a {
  background: white;
}

.accordion-view #panels {
  display: none;
}

.accordion-view .panel {
  display: none;
  width: 100%;
  border: 1px solid;
  box-sizing: border-box;
  border-top: none;
  padding: 10px;
  font-size: 18px;
}

.accordion-view .panel.current {
  display: block;
}
#tab-container.accordion-view {
  width: 85%;
  margin: 0 auto;
}
.attributes .md {
  background-color: #eee;
  padding: 4px;
  color: #000;
}

Copy and Paste Full Page Example