Create a JavaScript tab component with an adaptive stepper UI

Create a JavaScript tab component with an adaptive stepper UI


In the past, I’ve shown you how to create different tabbed interfaces. Today, we’ll build another responsive JavaScript tab component where the clickable tabs will appear as a stepper component.

If you aren’t familiar with stepper components, their primary goal is to improve the user experience by organizing large logical content blocks into smaller sequential steps. A widespread use case of such a component is the creation of a multi-step checkout in eCommerce sites.

Our Tab Component

Here’s what we’re going to create—resize your browser to see how the tab layout changes:

layout variantslayout variantslayout variants
Layout variants

We won’t focus so much on accessibility in this tutorial, so exploring how to make this component more accessible would be a valid next step. 

1. Begin with the page markup

Inside a container, we’ll place two lists that include the tabs and their associated content (panels).

By default, the first tab will be active.

Here’s the required markup:

1

class="grid">

2
  
3
    
  • class="active">
  • 4
           href="">
    
    5
             class="dot">
    
    6
            ...
    
    7
          
    
    8
        
    
    9
        
    
    10
      
    
    11
      
    12
        
  • class="active">...
  • 13
        
    
    14
      
    
    15
    
    

    2. Add the CSS

    Let’s concentrate on the main styles—you can see all of them by clicking on the CSS tab of the demo project.

    On large screens (>700px), the tab component will be like this:

    The desktop layout of our tab componentThe desktop layout of our tab componentThe desktop layout of our tab component

    On smaller ones, it’ll look like this:

    The mobile layout of our tab componentThe mobile layout of our tab componentThe mobile layout of our tab component

    Notice how the stepper switches between horizontal and vertical orientation depending on the screen size.

    Also, consider that all tab panels will be stacked and moved away 100% to the left; at any time, only the one with the active class will appear and sit in its initial position. 

    Here’s a part of the required styles:

    1
    /*CUSTOM VARIABLES HERE*/
    
    2
    
    
    3
    .grid {
    
    4
      display: grid;
    
    5
      grid-template-columns: auto auto;
    
    6
      gap: 70px;
    
    7
      max-width: 1000px;
    
    8
      padding: 0 20px;
    
    9
      margin: 0 auto;
    
    10
    }
    
    11
    
    
    12
    .tab-list li {
    
    13
      display: flex;
    
    14
    }
    
    15
    
    
    16
    .tab-list li:not(:last-child) {
    
    17
      margin-bottom: 40px;
    
    18
    }
    
    19
    
    
    20
    .tab-list a {
    
    21
      display: inline-flex;
    
    22
      align-items: center;
    
    23
      gap: 24px;
    
    24
      text-decoration: none;
    
    25
    }
    
    26
    
    
    27
    .tab-list a .dot {
    
    28
      position: relative;
    
    29
      display: inline-block;
    
    30
      width: 32px;
    
    31
      height: 32px;
    
    32
      border-radius: 50%;
    
    33
      border: 1px solid var(--stepper-outline-color);
    
    34
    }
    
    35
    
    
    36
    .tab-list li a .dot::before,
    
    37
    .tab-list li:not(:last-child) a .dot::after {
    
    38
      content: "";
    
    39
      position: absolute;
    
    40
      left: 50%;
    
    41
    }
    
    42
    
    
    43
    .tab-list li a .dot::before {
    
    44
      top: 50%;
    
    45
      transform: translate(-50%, -50%) scale(0);
    
    46
      width: 18px;
    
    47
      height: 18px;
    
    48
      border-radius: 50%;
    
    49
      background: var(--stepper-active-color);
    
    50
      transition: transform 0.3s;
    
    51
    }
    
    52
    
    
    53
    .tab-list li:not(:last-child) a .dot::after {
    
    54
      top: calc(100% + 1px);
    
    55
      transform: translateX(-50%);
    
    56
      height: 40px;
    
    57
      border-left: 2px dashed var(--stepper-connector-color);
    
    58
    }
    
    59
    
    
    60
    .tab-list li.active a {
    
    61
      font-weight: bold;
    
    62
    }
    
    63
    
    
    64
    .tab-list li.active a .dot::before {
    
    65
      transform: translate(-50%, -50%) scale(1);
    
    66
    }
    
    67
    
    
    68
    .tab-panels {
    
    69
      display: grid;
    
    70
      overflow: hidden;
    
    71
    }
    
    72
    
    
    73
    .tab-panels > li {
    
    74
      grid-area: 1/1;
    
    75
      opacity: 0;
    
    76
      transform: translateX(-100%);
    
    77
      transition: opacity 0.35s ease-in-out, transform 0.7s ease-in-out;
    
    78
    }
    
    79
    
    
    80
    .tab-panels > li.active {
    
    81
      opacity: 1;
    
    82
      transform: none;
    
    83
    }
    
    84
    
    
    85
    @media (max-width: 700px) {
    
    86
      .grid {
    
    87
        grid-template-columns: 1fr;
    
    88
        gap: 30px;
    
    89
      }
    
    90
    
    
    91
      .tab-list {
    
    92
        display: flex;
    
    93
        justify-content: center;
    
    94
      }
    
    95
    
    
    96
      .tab-list li:not(:last-child) {
    
    97
        margin: 0 40px 0 0;
    
    98
      }
    
    99
    
    
    100
      .tab-list li a span:last-child {
    
    101
        display: none;
    
    102
      }
    
    103
    
    
    104
      .tab-list a {
    
    105
        gap: 0;
    
    106
      }
    
    107
    
    
    108
      .tab-list li:not(:last-child) a .dot::after {
    
    109
        top: 50%;
    
    110
        left: calc(100% + 1px);
    
    111
        transform: translateY(-50%);
    
    112
        width: 40px;
    
    113
        height: auto;
    
    114
        border-bottom: 2px dashed var(--stepper-connector-color);
    
    115
        border-left: 0;
    
    116
      }
    
    117
    }
    

    3. Add the JavaScript

    Each time we click on a tab link, we’ll remove the active class from the currently active tab and panel. Then, we’ll put that class in the tab and panel associated with that link.

    Here’s the required JavaScript:

    1
    const tabList = document.querySelector(".tab-list");
    
    2
    const tabItems = tabList.querySelectorAll("li");
    
    3
    const tabLinks = tabList.querySelectorAll("a");
    
    4
    const tabPanelsList = document.querySelector(".tab-panels");
    
    5
    const tabPanels = tabPanelsList.querySelectorAll("li");
    
    6
    const ACTIVE_CLASS = "active";
    
    7
    
    
    8
    for (const tabLink of tabLinks) {
    
    9
      tabLink.addEventListener("click", function (e) {
    
    10
        e.preventDefault();
    
    11
        tabList.querySelector(`li.${ACTIVE_CLASS}`).classList.remove(ACTIVE_CLASS);
    
    12
        tabPanelsList
    
    13
          .querySelector(`li.${ACTIVE_CLASS}`)
    
    14
          .classList.remove(ACTIVE_CLASS);
    
    15
    
    
    16
        const parent = tabLink.parentElement;
    
    17
        let parentIndex = Array.from(tabItems).indexOf(parent);
    
    18
        parent.classList.add(ACTIVE_CLASS);
    
    19
        tabPanelsList
    
    20
          .querySelector(`li:nth-child(${++parentIndex})`)
    
    21
          .classList.add(ACTIVE_CLASS);
    
    22
      });
    
    23
    }
    

    Add keyboard support

    Although our component isn’t optimized for accessibility, let’s add support for keyboard navigation.

    On small screens, each time the left (←) or right (→) arrow keys are pressed, we’ll grab the currently active tab. From there, we’ll check to see which arrow is clicked. If that’s the right arrow, we’ll set the next active tab as the one that immediately follows the current active tab. If there isn’t such a tab, the next tab becomes the first one. Similarly, if the left arrow is clicked, we’ll set the next tab as the one that immediately precedes the currently active tab. If there isn’t such a tab, the next tab becomes the last one.

    We’ll follow the same process with the up (↑) and down (↓) keys on large screens. 

    Here’s the relevant JavaScript code:

    1
    ...
    
    2
    
    
    3
    tabList.addEventListener("keyup", function (e) {
    
    4
      const activeTabListItem = tabList.querySelector(`li.${ACTIVE_CLASS}`);
    
    5
    
    
    6
      if (
    
    7
        e.key === "ArrowUp" ||
    
    8
        e.key === "ArrowDown" ||
    
    9
        e.key === "ArrowLeft" ||
    
    10
        e.key === "ArrowRight"
    
    11
      ) {
    
    12
        if (
    
    13
          (mqSm.matches && (e.key === "ArrowUp" || e.key === "ArrowDown")) ||
    
    14
          (mqLg.matches && (e.key === "ArrowLeft" || e.key === "ArrowRight"))
    
    15
        ) {
    
    16
          return;
    
    17
        }
    
    18
    
    
    19
        if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
    
    20
          const prevActiveTabListItem = activeTabListItem.previousElementSibling
    
    21
            ? activeTabListItem.previousElementSibling
    
    22
            : lastTabListItem;
    
    23
          prevActiveTabListItem.querySelector("a").click();
    
    24
        } else {
    
    25
          const nextActiveTabListItem = activeTabListItem.nextElementSibling
    
    26
            ? activeTabListItem.nextElementSibling
    
    27
            : firstTabListItem;
    
    28
          nextActiveTabListItem.querySelector("a").click();
    
    29
        }
    
    30
      }
    
    31
    });
    

    Conclusion

    Congrats, folks! We built this beautiful and unique responsive JavaScript tab component without writing much code. From there, you can use it as it is and make it more accessible by checking the code of a similar component like Bootstrap’s tabs.

    Alternatively, you can isolate the tab list layout that looks like a stepper component and use it as you wish by adding functionality for navigation arrows, etc.

    Before closing, let’s recall what we created today:

    As always, thanks a lot for reading!



    Source link