Responsive Tabs (using CSS, JS)

by Konstantin Rouda

HTML

<main>

  <section class="c-tabs jsTabsContainer">
  
    <!--Tab with Paragraph content-->
    <article class="c-tabs__item jsTabItem is-active">
      
      <button type="button" class="c-tabs__title jsTabTitle" aria-expanded="true" aria-controls="paragraph-content-tab" role="tab" id="tab1">Paragraph</button>
      
      <div class="c-tabs__content" id="paragraph-content-tab" role="tabpanel" aria-labelledby="tab1" aria-hidden="false">
        <p>
          Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
        </p>
      </div>
      
    </article>
    
    <!--Tab with Unordered list content-->
    <article class="c-tabs__item jsTabItem">
      
      <button type="button" class="c-tabs__title jsTabTitle" aria-expanded="false" aria-controls="ul-content-tab" role="tab" id="tab2">Unordered List</button>
      
      <div class="c-tabs__content" id="ul-content-tab" role="tabpanel"  aria-labelledby="tab2"  aria-hidden="true">
          <ul>
             <li>Morbi in sem quis dui placerat ornare. Pellentesque odio nisi, euismod in, pharetra a, ultricies in, diam. Sed arcu. Cras consequat.</li>
             <li>Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi,...

CSS

HTML {
  font-family: 'Roboto', sans-serif;
  font-size: 100%;
  line-height: 1.5;
  box-sizing: border-box;
}

BODY {
  margin: 0;
  color: #3a3d40;
}

*, *::before, *::after {
  box-sizing: inherit;
  color: inherit;
}

MAIN {
    padding: 1.5em .1em;
}

:root {
  --tab-margin-left: 0;  
}

/*Actual Style*/

/************************
         Tabs
************************/
.c-tabs {
  position: relative;
  max-width: 950px; height: 700px;
  margin: 0 auto;
  //border: 2px solid;
}

/************************
      Tabs Item
************************/

.c-tabs__item {
  width: 100%;
  background: rgb(248, 248, 248);
  border-bottom: 1px solid rgb(143, 201, 208);
}

.c-tabs__item.is-active {
  border: none;
}

/************************
      Tabs Title
************************/

.c-tabs__title {
  display: block;
  width: 100%;
  margin: 0;
  padding: 1.5em 2em;
  border: none;
  font-size: 1.17rem;
  line-height: inherit;
  text-align: left;
  font-weight: bold;
  background: rgb(177, 233, 240);
  color: rgb(123, 151, 154);
  cursor: pointer;
  transition-property: background, color;
  transition-duration: .3s;
  transition-timing-function: ease-in-out;
}

.c-tabs__item.is-active > .c-tabs__title {
  background: rgb(1, 186, 210);
  color: #fff;
}

/************************
      Tabs Content
************************/

.c-tabs__content { 
  max-height: 0px;
  padding-top: 0;
  overflow-y: auto;
  visibility: hidden;
  transition: max-height .2s linear, visibility .1s linear;
}

.c-tabs__item.is-active > .c-tabs__content {
  max-height: 31.25rem; /*~500px*/ /*we also can set it to 100vh so it will get as much height as content needs*/
  padding: 1em;
  visibility: visible;
  transition-delay: 0s, .01s;
}



/************************
    @Media Queries
************************/

@media screen and (min-width: 750px) {
  
  /************************
          Main
  ************************/
  MAIN {
    padding: 1.5em 2em;
  }
  
  /************************
         ...

JavaScript

;(function(){
  "use strict";
  
  const tabsContainer = document.querySelector(".jsTabsContainer");
  //const tabs = tabsContainer.querySelectorAll(".jsTabItem");
  const tabsTitles = Array.from(tabsContainer.querySelectorAll(".jsTabTitle"));
  
  const matchMediaEl = window.matchMedia("(min-width: 750px)");
  
  const TAB_TITLE_CLASS_ID = "jsTabTitle";
  const TAB_ITEM_IS_ACTIVE_CLASS = "is-active";
  const TAB_MARGIN_LEFT_CUSTOM_PROP = "--tab-margin-left";
  
  const DATA_TYPE_ATTRIBUTE_NAME = "data-type";
  const DATA_TYPE_ACCORDION = "accordion";
  const DATA_TYPE_TAB = "tab";
  
  const TAB_TITLE_INDEX_PROP_NAME = "currentIndex";
  
  const KEYS = {
    UP: 38,
    DOWN: 40,
    LEFT: 37,
    RIGHT: 39,
    HOME: 36,
    END: 35,
    SPACE: 32
  };
  
  /*last active tab (initial value first tab)*/
  let lastActiveTab = tabsTitles[0]; 
  
  
  /************************
    Adding EventListeners
  *************************/
  tabsTitles.forEach(function(v, i) {
    v.addEventListener("click", tabItemOnClick, true);
    v[TAB_TITLE_INDEX_PROP_NAME] = i;
  });
  
  matchMediaEl.addListener(onWindowWidthChanged);
  
  /// we call the listener directly. This makes our listener perform adjustments based on the current device width.
  onWindowWidthChanged(matchMediaEl); 
  
  tabsContainer.addEventListener("keyup", tabsOnKeyUp, true);
  
  /************************
       Event Listeners
  *************************/
  /*
  * Triggers click event when tab is clicked
  */
  function tabItemOnClick(e) {
    let currentTab = e.target,
        currentTabParent = currentTab.parentElement;
    
    if(!currentTab.classList.contains(TAB_TITLE_CLASS_ID)) 
    { return false; };
    
    tabsTitles.forEach(function(v) {
      
      if(v === currentTab) {
          /*if tabs are displayed as tabs, we don't need to toggle is-active class, only add it to the active tab*/
          /*otherwise if the are displayed as accordion, we will toggle is-active class for the current...